nsqio/nsq · warning · FatalClientErr

exiting

Error message

exiting

What it means

Topic.PutMessage (nsqd/topic.go) takes a read lock and first checks the topic's exitFlag, an atomically-loaded int32 set when the topic is being deleted or nsqd is shutting down. If the flag is set, the write is refused with a plain 'exiting' error and the message is never enqueued; per-topic counters are not incremented. It is a lifecycle guard, not data corruption — the topic simply refuses new writes while it terminates.

Source

Thrown at nsqd/topic.go:187

	// update messagePump state
	select {
	case t.channelUpdateChan <- 1:
	case <-t.exitChan:
	}

	if numChannels == 0 && t.ephemeral {
		go t.deleter.Do(func() { t.deleteCallback(t) })
	}

	return nil
}

// PutMessage writes a Message to the queue
func (t *Topic) PutMessage(m *Message) error {
	t.RLock()
	defer t.RUnlock()
	if atomic.LoadInt32(&t.exitFlag) == 1 {
		return errors.New("exiting")
	}
	err := t.put(m)
	if err != nil {
		return err
	}
	atomic.AddUint64(&t.messageCount, 1)
	atomic.AddUint64(&t.messageBytes, uint64(len(m.Body)))
	return nil
}

// PutMessages writes multiple Messages to the queue
func (t *Topic) PutMessages(msgs []*Message) error {
	t.RLock()
	defer t.RUnlock()
	if atomic.LoadInt32(&t.exitFlag) == 1 {
		return errors.New("exiting")
	}

View on GitHub (pinned to 85cf10c09c)

Solutions

  1. Treat this as transient: stop publishing to the topic, let deletion/shutdown finish, and recreate/re-resolve the topic before retrying.
  2. For ephemeral topics, ensure at least one durable subscriber channel exists (do not let all channels be ephemeral) if you publish continuously.
  3. In producers using go-nsq, rely on the Producer's built-in reconnect: it re-establishes and retries after the daemon restarts.
  4. Sequence shutdown correctly: quiesce producers (or use a health gate) before sending SIGTERM to nsqd if you need loss-free in-flight writes.

Example fix

// before: ignoring the error during shutdown/deletion races
_ = topic.PutMessage(msg)

// after: check the lifecycle error and re-resolve the topic
if err := topic.PutMessage(msg); err != nil {
    if err.Error() == "exiting" {
        topic = nsqd.GetTopic(topicName) // topic was deleted/restarting; retry once
        err = topic.PutMessage(msg)
    }
    if err != nil {
        return err
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// producer-side gate before publishing during deploys
if !nsqdHealthy(addr) { // e.g. GET /info via HTTP health port
    waitOrPanic()
}

Try / catch

// go-nsq Producer already maps connection-level failures to retries; for direct
// topic API use, treat "exiting" as retriable:
err := topic.PutMessage(m)
if err != nil && err.Error() == "exiting" {
    topic = getOrCreateTopic(name) // re-resolve after delete/shutdown race
    err = topic.PutMessage(m)
}

Prevention

When it happens

Trigger: A producer publishes (PUB via the topic, HTTP /pub, or internal PutMessage from another component) concurrently with topic deletion — including automatic deletion of ephemeral topics when the last channel disappears (t.deleteCallback fires via the deleter) — or while nsqd is in Exit()/main shutdown, which sets exitFlag on every topic before flushing. HTTP producers get the raw error; TCP producers see it surfaced through the V2 protocol error path.

Common situations: Load tests that stop nsqd while producers are still connected; ephemeral topics (e.g. nsq_to_nsq or ephemeral channels with #ephemeral suffix) whose channels all disconnect while a producer keeps publishing; scripts publishing during topic deletion via the HTTP /topic/delete endpoint; graceful-shutdown races in CI.

Related errors


AI-assisted analysis of nsqio/nsq@85cf10c09c (2026-08-16). Data as JSON: /api/errors/4f344fd135838406. Report an issue: GitHub.