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
- Treat this as transient: stop publishing to the topic, let deletion/shutdown finish, and recreate/re-resolve the topic before retrying.
- For ephemeral topics, ensure at least one durable subscriber channel exists (do not let all channels be ephemeral) if you publish continuously.
- In producers using go-nsq, rely on the Producer's built-in reconnect: it re-establishes and retries after the daemon restarts.
- 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
- Quiesce producers before stopping/deleting nsqd or topics in runbooks.
- Avoid publishing to ephemeral topics unless a durable channel exists.
- Use go-nsq Producer, which reconnects and retries across nsqd restarts.
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.