Skip to main content

Troubleshooting

Use whensomething is already broken and you want the cause.

You getsymptoms grouped by area, each with its usual causes and the command that confirms them.

Connection errors​

NATS connection refused​

Startup throws NATS connection refused: <servers> when a critical connection cannot reach the server. A connection with critical: false retries in the background instead (see Multiple connections).

Causes: the server is not running, the URL in servers is wrong for this environment, or a firewall or network policy blocks the port.

Fix:

nats server check connection --server nats://localhost:4222

Load per-environment URLs with forRootAsync():

JetstreamModule.forRootAsync({
name: 'my-service',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
servers: config.get<string>('NATS_SERVERS')!.split(','),
}),
})

Reconnection loop​

The transport reconnects without limit (maxReconnectAttempts: -1). Repeated reconnects mean the server is flapping or something drops the connection.

Diagnosis:

  1. Register a Reconnect hook to see when reconnections happen:

    hooks: {
    [TransportEvent.Reconnect]: (server) => console.log(`Reconnected to ${server}`),
    }
  2. Check NATS server logs for client disconnect reasons.

  3. Verify TLS certificates haven't expired.

Consumer issues​

Messages not being delivered​

Checklist:

  1. Handler registered? The startup log prints Registered handlers: X RPC, Y events, Z broadcasts, plus N ordered and N scheduled when present. A zero count means the decorator never reached the registry: an import-order or module-wiring problem.
  2. Stream and consumer exist? nats stream ls, then nats consumer ls <stream-name>.
  3. Subject matches? nats sub "servicename__microservice.ev.>" shows whether messages arrive on the expected subject.
  4. Publisher-only? With publisherOnly: true no handlers are registered.
  5. Right connection? With several connections, a handler consumes only from the connection it is bound to.

Messages redelivered unexpectedly​

Messages are redelivered when the ack_wait deadline expires before the handler acknowledges them.

Causes:

  • The handler outlives ack_wait
  • The handler throws, so the message is retried after the retry delay
  • The connection dropped during processing

Fix: raise ack_wait (consumer: { ack_wait: toNanos(60, 'seconds') }), enable ackExtension: true for long handlers, or cap concurrency to prevent overload. See Performance Tuning.

Consumer lag growing​

  1. Profile the handler first: database queries, external API calls and heavy computation are the usual bottlenecks.
  2. Raise concurrency (events: { concurrency: 200 }) and max_ack_pending (consumer: { max_ack_pending: 500 }).
  3. Scale horizontally: each instance takes a share of the workqueue.

Watch the lag:

nats consumer info <stream> <consumer> | grep "Num Pending"

RPC issues​

RPC timeout errors​

The caller gets an rpc.timeout error when no response arrives in time. The default is 30 s in Core mode and 3 min in JetStream mode, and the deadline also covers waiting for the connection.

Causes:

  • Handler is slow or stuck
  • No handler registered for the pattern
  • Network partition between publisher and consumer
  • Wrong RPC mode: publisher uses core but handler expects jetstream (or vice versa)

Diagnosis:

hooks: {
[TransportEvent.RpcTimeout]: (subject, correlationId) =>
console.warn(`RPC timeout: ${subject} (${correlationId})`),
}

Fix:

  • Raise the timeout: rpc: { mode: 'core', timeout: 60_000 }, or per request with setTimeout()
  • Use the same RPC mode on both sides

No handler for subject errors​

A message arrived on a subject with no registered handler. An event in that state becomes a dead letter at once (see Dead Letter Queue). A Core RPC request gets an error reply.

Causes:

  • Typo in the @EventPattern() or @MessagePattern() pattern
  • Handler not imported in the module
  • Subject naming mismatch between publisher and consumer

Fix: Compare the full NATS subject in the warning with your handler patterns. See Naming Conventions for the subject structure.

Dead letter queue​

Dead letters are missing​

The DLQ stream is on by default and catches workqueue and broadcast events that exhaust max_deliver (default 3), plus events with no handler or an undecodable payload. Check what applies:

  1. dlq: false? Then only the onDeadLetter callback, if any, sees dead letters.
  2. Global provisioning.management: Manual? The implicit DLQ stays off. Configure dlq to bind an external stream.
  3. RPC or ordered? Neither dead-letters: RPC returns errors to the caller, and ordered events have no ack cycle.
  4. max_deliver unlimited on an external consumer? Nothing ever exhausts, and the binder warns at boot.

DLQ stream publish fails​

The transport retries the publish, falls back to onDeadLetter, and finally nak()s the message so it stays in the source stream. The full fallback chain has the order.

Causes: the DLQ stream was deleted (nats stream rm orders__microservice_dlq-stream), the server is out of disk or the stream hit max_bytes, or the connection dropped before the republish.

Fix:

  1. Check the stream: nats stream ls | grep dlq-stream.
  2. If it was deleted, restart the pod: in Auto mode startup recreates it.
  3. Check server disk usage: nats server report accounts.

Handler metadata registry​

Entries missing from the KV bucket​

The transport only publishes handler metadata when the handler has a meta field in its decorator extras. Handlers without meta are intentionally skipped, see Handler Metadata for the quick-start example.

Checklist:

  1. Does the handler have meta: { ... } in @EventPattern / @MessagePattern?
  2. Did startup succeed? Check logs for MetadataRegistry errors.
  3. Inspect the bucket: nats kv ls handler_registry

Bucket config mismatch error on startup​

NATS KV buckets have immutable config for some fields (replicas, ttl). If you change these in forRoot() after the bucket already exists, startup fails.

Fix: Delete your configured metadata bucket. The default name is handler_registry, so substitute your own if you overrode metadata.bucket in forRoot(). Entries are re-published on the next startup, which makes the delete safe.

# Replace `handler_registry` with your metadata.bucket value if you overrode it
nats kv rm handler_registry

# Restart the service → fresh bucket with new config

Stream migration​

Consumer self-healing waits on "migration in progress"​

An interrupted migration (process killed mid-phase, NATS crash) leaves a {stream}__migration_backup stream behind. Consumer self-healing, which runs after a live consumer's iterator breaks, refuses to recreate the consumer while that backup exists and retries with backoff. Initial startup does not run this check.

Diagnosis:

nats stream ls | grep migration_backup

Fix: usually none: self-healing recovers once the migrating pod finishes and removes the backup, and the next startup of any pod finishes an interrupted restore. If the backup is stranded for good, inspect it, then keep it (it may hold messages you need) or delete it with nats stream rm <stream>__migration_backup. See Stream Migration, Error handling.

Publisher errors during rolling update​

From the quiesce until the restore completes, publishes to the migrating stream reject with "no stream matches subject" or no-responders errors. Retry in the caller, or pause publishers during a maintenance window. See what publishers see.

Startup issues​

Unable to resolve signature of method decorator on @EventPattern​

NestJS 12 adds a typed @EventPattern overload, and a plain string topic matches it. Under strict TypeScript that overload accepts only a handler shaped exactly (data, ...args: unknown[]), so a handler with no parameters, or one that takes @Ctx() ctx: RpcContext, fails to compile with error TS1241.

Fix: pass the topic type explicitly to select the untyped overload. It exists in NestJS 10 and 11 too, so the same code compiles on every supported major:

@EventPattern<string>('order.created')
handle(@Payload() data: OrderCreated, @Ctx() ctx: RpcContext): void {}

@MessagePattern has no typed overload and is unaffected.

JetstreamCapabilityError​

Stream orders__microservice_ev-stream (allow_msg_ttl) needs NATS JetStream API level 1 (NATS 2.11 or newer); nats-1 (NATS 2.10.24) reports level 0.

A feature needs a newer server than the one the client talks to: ttl() / allow_msg_ttl need 2.11, scheduleAt() / allow_msg_schedules 2.12, cron and interval schedules 2.14. Older servers accept these settings and silently ignore them, so the transport fails the boot or the call instead. Upgrade every node in the cluster, or drop the feature. See NATS server versions.

Boot fails in Manual mode​

A bound stream or consumer is missing, or its subjects or filter do not cover the handlers. The error names the entity and the fix. Boot validation lists every check.

listen() hangs on startup​

  1. NATS connection timeout: the initial connection attempt blocks startup.
  2. Ordered consumer can't connect: check that the ordered stream exists.

transport-controlled config conflicts​

Stream orders__microservice_ev-stream has transport-controlled config conflicts that cannot be migrated: retention: ...

The existing stream has a retention the transport does not use for that kind, usually because something else created it. Other immutable changes (such as storage) only log a warning unless you enable stream migration.

Fix: delete the stream and let the transport recreate it:

nats stream rm <stream-name>
warning

Deleting a stream destroys all messages in it. Only do this in development or when data loss is acceptable.

Scheduling​

JetstreamScheduleError with reason server-timezone-data​

The NATS server rejected the time zone of <subject>. The official scratch images carry no time zone data; run an alpine image, or schedule in UTC.

The client accepted the IANA time zone of a cron schedule, but the server has no time zone database. Run the nats:<version>-alpine image, or drop the timezone and schedule in UTC. See Recurring schedules, Requirements.

Typed error handling with NatsErrorCode​

To react to a JetStream API error in your own code, compare its code with the exported NatsErrorCode enum instead of matching on messages. It lists the codes the transport itself reacts to:

import { NatsErrorCode } from '@horizon-republic/nestjs-jetstream';

try {
await jsm.streams.info('orders__microservice_ev-stream');
} catch (err) {
const code = (err as { code?: number }).code;

if (code === NatsErrorCode.StreamNotFound) {
// 10059, stream does not exist yet, safe to create
} else if (code === NatsErrorCode.ConsumerNotFound) {
// 10014, consumer was deleted externally
} else if (code === NatsErrorCode.ConsumerAlreadyExists) {
// 10148, race on consumer create, fetch the existing one instead
} else {
throw err;
}
}

See also​