nats-io/nats-server · error · JSStreamPurgeFailedError

JS_STREAM_PURGE_FAILED

JS_STREAM_PURGE_FAILED

Error message

stream purge not permitted

What it means

A JetStream API error (JS_STREAM_PURGE_FAILED) returned by the stream purge request handler when the stream's configuration denies purging (DenyPurge). The purge request is rejected upfront with the explicit 'stream purge not permitted' error rather than attempting the operation.

Source

Thrown at server/jetstream_api.go:4034

		purgeRequest = &req
	}

	mset, err := acc.lookupStream(stream)
	if err != nil {
		resp.Error = NewJSStreamNotFoundError(Unless(err))
		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
		return
	}
	mset.cfgMu.RLock()
	sealed, denyPurge := mset.cfg.Sealed, mset.cfg.DenyPurge
	mset.cfgMu.RUnlock()
	if sealed {
		resp.Error = NewJSStreamSealedError()
		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
		return
	}
	if denyPurge {
		resp.Error = NewJSStreamPurgeFailedError(errors.New("stream purge not permitted"))
		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
		return
	}

	if s.JetStreamIsClustered() {
		s.jsClusteredStreamPurgeRequest(ci, acc, mset, stream, subject, reply, rmsg, purgeRequest)
		return
	}

	purged, err := mset.purge(purgeRequest)
	if err != nil {
		resp.Error = NewJSStreamGeneralError(err, Unless(err))
	} else {
		resp.Purged = purged
		resp.Success = true
	}
	s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp))
}

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Update/recreate the stream with DenyPurge: false if purging is intended.
  2. Use max-age/max-msgs/limits-based retention to expire messages instead of explicit purge.
  3. Check StreamInfo().Config.DenyPurge before issuing purge calls and skip gracefully.
  4. If data must be removed from a DenyPurge stream, delete and recreate the stream (if policy allows).

Example fix

// before
_, err := js.PurgeStream("AUDIT") // DenyPurge stream
// after
si, _ := js.StreamInfo("AUDIT")
if si.Config.DenyPurge {
    return fmt.Errorf("stream AUDIT denies purge")
}
_, err := js.PurgeStream("AUDIT")
Defensive patterns

Strategy: validation

Validate before calling

si, err := js.StreamInfo(name)
if err != nil { return err }
if si.Config.DenyPurge { return fmt.Errorf("stream %s denies purge", name) }

Type guard

func canPurge(si *nats.StreamInfo) bool { return si != nil && !si.Config.DenyPurge }

Try / catch

_, err := js.PurgeStream(name)
var apiErr *nats.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode == nats.JSStreamPurgeFailed {
    return fmt.Errorf("purge not permitted on %s: %w", name, err)
}

Prevention

When it happens

Trigger: Sending $JS.API.STREAM.PURGE.<stream> for a stream whose StreamConfig has DenyPurge: true; the denyPurge flag is derived from config and short-circuits before plain or clustered purge execution (same pattern as sealed-stream rejection just above it).

Common situations: Purging an audit/compliance stream created with DenyPurge; environments where streams were recreated with stricter retention flags than the tooling expects; scripts cleaning test streams hitting production-like configs.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/a865ca5899105f2d. Report an issue: GitHub.