Your application connects to NATS successfully, then fails on the first persistence call: stream creation, consumer creation, or a publish that waits for an ack. The client error is one of two strings: nats: jetstream not enabled for account (err_code 10039) or nats: jetstream not enabled (err_code 10076). Core pub/sub keeps working the whole time, which is what makes this confusing during an incident.
The server is healthy, clients are connected, and messages route fine. What you have is a configuration mismatch: the client is issuing JetStream operations against a server where JetStream was never enabled, or against an account that was never granted JetStream resources. Nothing will self-heal; the fix is always a configuration change.
What this means
JetStream is opt-in at two independent levels:
- Server level. JetStream must be enabled on the server itself, via a
jetstream {}block in the config file or the-js/--jetstreamcommand-line flag. Without this, no account on the server can use persistence, and every JetStream API call returns err_code 10076 (“jetstream not enabled”). - Account level. Even on a JetStream-enabled server, each account needs its own JetStream limits (memory storage, file storage, stream count, consumer count). An account without those limits gets err_code 10039 (“JetStream not enabled for account”). The account designated as the system account cannot have JetStream enabled at all.
Because core NATS has no dependency on JetStream, a server in this state passes most health checks, accepts connections, and routes messages normally. The only failing surface is the JetStream API, which is exactly what your client hit.
flowchart TD
A[Client calls JetStream API] --> B{Which error?}
B -->|"10076: jetstream not enabled"| C[Server has no jetstream block or -js flag]
B -->|"10039: not enabled for account"| D{Which account is the client bound to?}
D -->|"$G global account"| E[Client not mapped to a named account - fell through to $G]
D -->|"Named account"| F{Account has JS limits?}
F -->|"No"| G[Grant limits: config block or nsc edit]
F -->|"Yes, but too small"| H[Server log: insufficient memory resources]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| JetStream never enabled on the server | All clients, all accounts get “jetstream not enabled” (10076) | Config file for a jetstream block, or -js on the process command line |
| Server enabled, account has no JetStream limits | Only clients in that account get 10039 | Account block in config, or account JWT in operator mode |
Client bound to the $G global account | 10039 even though account config “looks right” | Whether the client’s connection actually maps to a user in a named account |
nsc edit account --js-enable without storage flags | 10039 persists after “enabling” JetStream via nsc | Whether --js-mem-storage and --js-disk-storage were also set |
Config reload removed the jetstream block | JetStream worked before, calls now fail, stored data still on disk | /jsz disabled field plus recent uptime/reload history |
| Account storage limit too small for existing streams | Server log shows “insufficient memory resources available” | Server log at startup or JWT push time |
Competing top-level authorization block | Users land in the wrong account, then get 10039 | Whether config mixes top-level authorization with account-level users |
Quick checks
All of these are read-only.
# Is JetStream enabled on this server at all?
curl -s http://localhost:8222/jsz | jq .disabled
# Lighter check: is JetStream enabled (no deep health validation)?
curl -s http://localhost:8222/healthz?js-enabled-only=true
# Recent restart or reload? A fresh uptime plus a newly disabled
# JetStream points at a config change, not a crash.
curl -s http://localhost:8222/varz | jq .uptime
# Are JetStream API errors accumulating?
curl -s http://localhost:8222/jsz | jq '{api_total: .api.total, api_errors: .api.errors}'
# Look for account-level JetStream config failures in the server log
# (adjust the log path to your deployment)
grep -i "jetstream" /var/log/nats/nats-server.log | grep -iE "error|insufficient|disabled" | tail -20
# Confirm the running process actually has JetStream config
# (adjust the config path to your deployment)
grep -A5 "jetstream" /etc/nats/nats-server.conf
Two things to note while checking. First, /jsz disabled=true tells you JetStream is off, but says nothing about why; the log and config tell you why. Second, if /jsz returns an error or empty response entirely, the monitoring port may not be enabled (-m 8222 or http_port: 8222), which is a separate gap to fix.
How to diagnose it
- Read the error code carefully. 10076 means the server has no JetStream at all. 10039 means the server may have JetStream, but the account your client is bound to does not. These have completely different fixes, so establish which one you have before changing anything.
- Check the server-level flag.
curl -s http://localhost:8222/jsz | jq .disabled. Ifdisabledistrueand you expected JetStream, the server was started without it or a reload removed it. - Identify which account the client actually bound to. This is the step everyone skips, and it is where most 10039 errors live. If your config defines accounts but the client connects in a way that does not map to any named account’s user list, the connection lands in the
$Gglobal account.$Ghas no JetStream limits by default, so every JetStream call fails with 10039 while core messaging works fine. Confirm the client’s credentials and which account they map to. - Check the account’s JetStream limits. In static config mode, the account block needs
jetstream: enabledor ajetstream { ... }section with storage budgets. In operator/JWT mode, the limits live in the account JWT, managed withnsc. A common trap:nsc edit account --js-enablealone does not set storage limits, and without non-zero--js-mem-storageand--js-disk-storagethe account still cannot use JetStream. - Check the server log for resource failures. If limits exist but JetStream still will not initialize for the account, look for
Error configuring jetstream for account ... insufficient memory resources available. This happens when the account’s memory storage limit is set too small (in the extreme case, a few bytes) for the streams it is supposed to host. - Check uptime against the failure timeline. If JetStream worked until recently and
/varzuptime is low, a restart or reload happened. JetStream configuration is not fully hot-reloadable: before NATS Server v2.14.0, changingmax_mem,max_file, orstore_dirvia SIGHUP did nothing; from v2.14.0,max_mem_storeandmax_file_storereload butstore_dirstill requires a full restart. Worse, if the server was started with--jetstreamas a flag and you later reload a config file without ajetstreamblock, JetStream gets disabled, because the flag no longer overrides anything. That sequence produces exactly this symptom with no crash and no obvious event.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
/jsz disabled | Tells you JetStream is enabled at the server level | true on a node expected to run JetStream, sustained past startup |
/healthz?js-enabled-only=true | Lightweight “is JetStream on” probe | Non-200 on a JetStream node after the startup grace window |
/jsz api.errors rate | Counts failed JetStream API calls, including the failures your clients are seeing | Sustained positive rate; a client retry loop against an unconfigured account inflates this fast |
/varz uptime | Correlates JetStream loss with a restart or reload | Uptime reset close in time to the first client errors |
| Server log, JetStream lines | The authoritative record of account-level config failures | “insufficient memory resources” or “operators do not allow Accounts to be configured directly” |
Do not rely on bare /healthz for this. On a JetStream server, bare /healthz runs full JetStream health validation and can fail during recovery even when JetStream is correctly enabled. For the “is it enabled” question specifically, use ?js-enabled-only=true. See NATS /healthz explained for the full semantics.
Fixes
JetStream not enabled on the server (10076)
Add a jetstream block to the server config and restart:
jetstream {
store_dir: /var/lib/nats/jetstream
max_mem: 1G
max_file: 10G
}
Or start with -js for a quick standalone test. This requires a restart, not a reload, so plan it: a restart drops every connected client. In a cluster, do a rolling restart so clients fail over to other nodes; on a standalone server, expect a brief full outage and confirm clients have reconnect logic with backoff. Stored stream data on disk survives; you are enabling the subsystem, not recreating it.
Account missing JetStream limits in static config mode (10039)
Inside the account block, grant limits explicitly:
accounts {
HR {
jetstream: enabled
users: [ {user: hr_client, password: ...} ]
}
}
Or with explicit budgets: jetstream { max_mem: 512M, max_file: 2G }. Make sure the client’s credentials match a user listed in that account. If the client connects without mapping to that account, it lands in $G and the account config you just wrote never applies to it.
Also remove any competing top-level authorization {} block. Mixing top-level authorization with account-level users lists configures two competing auth systems, and users can bind to the wrong account as a result.
Account missing limits in operator/JWT mode (10039)
Account limits live in the account JWT, not the server config. In fact, in operator mode the server rejects accounts configured directly in the config file (“operators do not allow Accounts to be configured directly”). Edit the account with nsc:
# Grant JetStream storage and asset limits to the HR account
nsc edit account --name HR \
--js-mem-storage 1G \
--js-disk-storage 512M \
--js-streams 10 \
--js-consumer 100
Use -1 for unlimited on the storage flags if that fits your policy. The key point: enablement alone is not sufficient. Without non-zero memory and disk storage limits, the account still cannot create streams, and you get the same 10039 error with a config that looks “enabled.”
Account storage limit too small
If the log shows “insufficient memory resources available” while configuring JetStream for an account, the account’s memory storage limit cannot hold the streams assigned to it. Raise the limit in the account config or JWT, push the updated JWT (operator mode), and reload or restart as required.
JetStream disabled by a config reload
If the server was running JetStream via the --jetstream flag and a config reload introduced a file without a jetstream block, add the block to the config file explicitly and restart. Treat flag-based startup as a dev convenience only; production servers should carry their JetStream config in the file so reloads cannot silently remove it.
Prevention
- Config in files, not flags. Put the
jetstreamblock in the server config. Flag-based enablement can be undone by a routine reload. - Bind every client to a named account explicitly. If you use accounts, every production client should present credentials that map to a specific account’s user list. Audit for clients falling through to
$G. - Grant storage limits, not just enablement. Whether via config or
nsc, always set memory and disk storage values. Enablement flags alone produce this exact error. - Alert on
/jszdisabled=truewhere JetStream is expected, gated on uptime past the startup grace window and sustained for a few minutes, so a bad config deploy pages before your clients do. - Track
/jszapi.errorsas a rate. A client retrying persistence calls against an unconfigured account shows up here immediately and points you at the failing account before the ticket queue fills up. - Restart after JetStream config changes. Do not assume SIGHUP applies JetStream changes.
store_dirrequires a restart even on current versions.
How Netdata helps
- Netdata polls the NATS monitoring endpoints directly, so the
/jszdisabledflag,api.total, andapi.errorsare charted continuously rather than checked by hand during an incident. - A rising
api.errorsrate correlated with flat message throughput and a recent uptime reset points to “config change disabled JetStream” in one glance, instead of a log archaeology session. - Alerting on
disabled=truewith uptime gating matches the paging guidance above and catches reload-induced disablement, which otherwise surfaces only as client-side errors. - Because Netdata also charts connections, throughput, and slow consumers, you can confirm the blast radius: core messaging unaffected, only the JetStream API erroring. That correlation is what separates this class of failure from a real outage.
Related guides
- NATS /healthz explained: js-server-only vs js-enabled-only vs the bare check
- How NATS actually works in production: a mental model for operators
- NATS monitoring checklist: the signals every production server needs
- NATS crash loop: unexpected uptime resets and repeated restarts
- NATS messages published but not received: subject mismatches and cross-server gaps
- NATS no responders available for request: request-reply into the void






