Per-Message TTL
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: trueon 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
| Scenario | TTL | Why |
|---|---|---|
| Session tokens | 30 minutes | Auto-expire inactive sessions |
| OTP codes | 5 minutes | Security — short-lived by design |
| Cache entries | 1 hour | Stale cache auto-cleans |
| Feature flags | 24 hours | Temporary overrides that self-remove |
| Rate limit counters | 1 minute | Rolling window without cleanup jobs |
How it works
.ttl(toNanos(30, 'minutes'))passes the TTL through to the NATS JetStream publish options- NATS sets the
Nats-TTLheader on the stored message (see ADR-43) - After 30 minutes, NATS automatically removes the message from the stream
- 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 withmax_age: 7 daysexpires 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 atmax_age(stream wins)
Limitations
| Limitation | Details |
|---|---|
| Events only | ttl() is ignored for RPC (client.send()); a warning is logged |
| NATS >= 2.11 | allow_msg_ttl is not supported by older server versions |
| Per-stream opt-in | Each stream must have allow_msg_ttl: true explicitly |
| No consumer-side awareness | Consumers don't know if a message has TTL — they process it normally before expiry |
See also
- Record Builder & Deduplication — full
JetstreamRecordBuilderAPI including.ttl(),.setMessageId(),.scheduleAt() - Scheduling (Delayed Jobs) — the sibling feature for one-shot delayed delivery
- Default Configs —
allow_msg_ttlin the enable-only stream properties table - Module Configuration — where to set
events.stream.allow_msg_ttl