slackhq/nebula · error
ErrMessageCounterExhausted
ErrMessageCounterExhausted
Error message
message counter exhausted
What it means
EncryptDanger refuses to encrypt once the message nonce reaches RejectAfterMessages (MaxUint64 minus RejectHeadroom of 2^40). Beyond that point the remaining nonce space is reserved for out-of-order receive window headroom, so further encryption would break replay protection; the connection must rekey.
Source
Thrown at noiseutil/cipher_state.go:18
package noiseutil
import (
"errors"
"fmt"
"math"
"github.com/flynn/noise"
)
// RejectHeadroom is the wrap gap for senders racing the counter, sized large enough for any routine count.
const RejectHeadroom = uint64(1) << 40
// RejectAfterMessages is the nonce ceiling: encrypting stops RejectHeadroom short of the wrap.
const RejectAfterMessages = math.MaxUint64 - RejectHeadroom
// ErrMessageCounterExhausted is returned by EncryptDanger once the nonce reaches RejectAfterMessages.
var ErrMessageCounterExhausted = errors.New("message counter exhausted")
// CipherState is the post-handshake AEAD cipher used for the data plane.
// Each supported cipher has its own concrete implementation in this package with the nonce endianness hardcoded,
// so the encrypt/decrypt fast path avoids interface dispatch on the byte order.
type CipherState interface {
// EncryptDanger encrypts and authenticates a given payload.
//
// out is a destination slice to hold the output of the EncryptDanger operation.
// - ad is additional data, which will be authenticated and appended to out, but not encrypted.
// - plaintext is encrypted, authenticated and appended to out.
// - n is a nonce value which must never be re-used with this key.
// - nb is a scratch buffer used to assemble the nonce.
EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error)
// DecryptDanger authenticates and decrypts a given payload, with the same argument shape as EncryptDanger.
DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error)
// Overhead returns the AEAD tag size, or 0 if the receiver is nil.View on GitHub (pinned to dd8f660c0a)
Solutions
- Force a handshake/rekey to establish fresh session keys before the nonce ceiling is reached.
- Check that the automatic rekey logic (key reload/rotate timers) is enabled and functioning.
- If this appears early in a connection's life, reset the nonce counter to 0 with the new session key.
Defensive patterns
Strategy: try-catch
Validate before calling
if n >= noiseutil.RejectAfterMessages {
return errors.New("nonce ceiling reached; rekey required")
} Try / catch
out, err := cs.EncryptDanger(out, ad, plaintext, n, nb)
if errors.Is(err, noiseutil.ErrMessageCounterExhausted) {
// initiate new handshake and retry on the fresh session
} Prevention
- Keep automatic rekey enabled and tested.
- Monitor session age/nonce usage on long-lived tunnels.
- Treat this error as 'must rekey', never retry on the same key.
When it happens
Trigger: A long-lived tunnel sends more than ~2^64-2^40 messages on the same session key and the nonce counter hits RejectAfterMessages; exercised by TestEncryptRejectsExhaustedCounter.
Common situations: Extremely long-lived connections without rekeying, embedded devices that never rotate keys, or test rigs manually passing enormous nonce values.
Related errors
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/750ebbc43927d223.
Report an issue: GitHub.