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
- Update/recreate the stream with DenyPurge: false if purging is intended.
- Use max-age/max-msgs/limits-based retention to expire messages instead of explicit purge.
- Check StreamInfo().Config.DenyPurge before issuing purge calls and skip gracefully.
- 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
- Check StreamInfo().Config.DenyPurge before purge operations
- Use retention limits (max_age/max_msgs) for automatic cleanup
- Keep stream config templates consistent across environments
- Treat purge denial as a permanent policy error, not retryable
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
- JS_STREAM_MSG_DELETE_FAILED
- JS_STREAM_ROLLUP_FAILED
- roll-ups require the purge permission
- could not create storage directory - %v
- storage directory is not writable
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/a865ca5899105f2d.
Report an issue: GitHub.