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

  1. Recreate/update the stream with DenyDelete: false if deletion is intended.
  2. Use stream purge or limit-based retention instead of per-message delete on DenyDelete streams.
  3. Choose a different stream (or a mirror with permissive policy) for deletable data.
  4. 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

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


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