nats-io/nats-server · error · JSStreamMsgDeleteFailedError
JS_STREAM_MSG_DELETE_FAILED
JS_STREAM_MSG_DELETE_FAILED
Error message
message delete not permitted
What it means
A JetStream API error (JS_STREAM_MSG_DELETE_FAILED) returned by the direct message delete request handler when the stream's configuration denies message deletion (DenyDelete). Even though the request is well-formed, the stream policy forbids removing individual messages, so the server rejects it with this explicit 'message delete not permitted' error.
Source
Thrown at server/jetstream_api.go:3598
return
}
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, denyDelete := mset.cfg.Sealed, mset.cfg.DenyDelete
mset.cfgMu.RUnlock()
if sealed {
resp.Error = NewJSStreamSealedError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if denyDelete {
resp.Error = NewJSStreamMsgDeleteFailedError(errors.New("message delete not permitted"))
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
if s.JetStreamIsClustered() {
s.jsClusteredMsgDeleteRequest(ci, acc, mset, stream, subject, reply, &req, rmsg)
return
}
var removed bool
if req.NoErase {
removed, err = mset.removeMsg(req.Seq)
} else {
removed, err = mset.eraseMsg(req.Seq)
}
if err != nil {
resp.Error = NewJSStreamMsgDeleteFailedError(err, Unless(err))
} else if !removed {View on GitHub (pinned to 3a66a489d2)
Solutions
- Recreate/update the stream with DenyDelete: false if deletion is intended.
- Use stream purge or limit-based retention instead of per-message delete on DenyDelete streams.
- Choose a different stream (or a mirror with permissive policy) for deletable data.
- Check stream config with StreamInfo before issuing deletes and handle the denial gracefully.
Example fix
// before
_, err := js.DeleteMsg("AUDIT", 42) // DenyDelete stream
// after
si, _ := js.StreamInfo("AUDIT")
if si.Config.DenyDelete {
return fmt.Errorf("stream AUDIT denies message deletion")
}
_, err := js.DeleteMsg("AUDIT", 42) Defensive patterns
Strategy: validation
Validate before calling
si, err := js.StreamInfo(name)
if err != nil { return err }
if si.Config.DenyDelete { return fmt.Errorf("stream %s denies message delete", name) }
Type guard
func canDeleteMsgs(si *nats.StreamInfo) bool { return si != nil && !si.Config.DenyDelete } Try / catch
_, err := js.DeleteMsg(name, seq)
var apiErr *nats.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode == nats.JSStreamMsgDeleteFailed {
// denied by DenyDelete; surface as policy error, not retryable
return fmt.Errorf("delete not permitted on %s: %w", name, err)
} Prevention
- Check StreamInfo().Config.DenyDelete before delete workflows
- Align stream templates across environments so DenyDelete matches expectations
- Document deletion policy per stream for tooling authors
- Prefer retention limits over per-message deletes on restricted streams
When it happens
Trigger: Sending $JS.API.STREAM.MSG.DELETE.<stream> (DirectDelete or plain) for a stream whose StreamConfig has DenyDelete: true; denyDelete is computed from the stream config and short-circuits before clustered/plain delete processing.
Common situations: Deleting messages from a stream created with DenyDelete for compliance/audit reasons; config drift where a stream was recreated with DenyDelete after tooling was written; copy/paste of stream configs between environments.
Related errors
- JS_STREAM_PURGE_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/923f49d16fbf859c.
Report an issue: GitHub.