Skip to main content

Per-Message TTL

Since v2.9.0

Individual message expiration via the Nats-TTL header (ADR-43). Each message can have its own lifetime, independent of the stream's max_age.

Requirements

  • NATS Server >= 2.11
  • allow_msg_ttl: true on the stream

Configuration

Enable per-message TTL on the event stream:

JetstreamModule.forRoot({
name: 'sessions',
servers: ['nats://localhost:4222'],
events: {
stream: { allow_msg_ttl: true },
},
});

This flag can be safely added to existing streams — NATS applies it as a regular update without recreation or downtime.

Usage

Use ttl() on JetstreamRecordBuilder with nanoseconds (via toNanos()):

import { JetstreamRecordBuilder, toNanos } from '@horizon-republic/nestjs-jetstream';
import { lastValueFrom } from 'rxjs';

const record = new JetstreamRecordBuilder({ token: 'abc123', userId: 42 })
.ttl(toNanos(30, 'minutes'))
.build();

await lastValueFrom(this.client.emit('session.token', record));

The consumer handles it like any normal event — no changes needed on the receiving side. After 30 minutes, NATS automatically removes the message from the stream.

Use cases

ScenarioTTLWhy
Session tokens30 minutesAuto-expire inactive sessions
OTP codes5 minutesSecurity — short-lived by design
Cache entries1 hourStale cache auto-cleans
Feature flags24 hoursTemporary overrides that self-remove
Rate limit counters1 minuteRolling window without cleanup jobs

How it works

  1. .ttl(toNanos(30, 'minutes')) passes the TTL through to the NATS JetStream publish options
  2. NATS sets the Nats-TTL header on the stored message (see ADR-43)
  3. After 30 minutes, NATS automatically removes the message from the stream
  4. If a consumer processes the message before expiry, it works normally

Important: max_age interaction

Per-message TTL works independently from stream max_age:

  • A message built with .ttl(toNanos(5, 'minutes')) in a stream with max_age: 7 days expires after 5 minutes
  • A message without TTL in the same stream expires after 7 days (stream default)
  • If the per-message TTL exceeds max_age, the message still expires at max_age (stream wins)

Limitations

LimitationDetails
Events onlyttl() is ignored for RPC (client.send()); a warning is logged
NATS >= 2.11allow_msg_ttl is not supported by older server versions
Per-stream opt-inEach stream must have allow_msg_ttl: true explicitly
No consumer-side awarenessConsumers don't know if a message has TTL — they process it normally before expiry

See also