Repository: nats-io/nats-server

Issue:
Updating account JWT with limits on the fly
### Observed behavior

I have a 6 node local cluster NATS setup with jetstream enabled. In this setup, I programmatically create account JWTs and push it to the NATS cluster using `$SYS.REQ.CLAIMS.UPDATE`. This works fantastic. 

I have a workflow in which the limits of the JWT (like number of allowed connections (conns)) are to be either increased or reduced. 

The way I do it is to recreate the JWT with the new limits and then push it to the cluster using `$SYS.REQ.CLAIMS.UPDATE`. 

This is the situation i'm in, 
1. There is an account which allows a maximum of 5 concurrent connections.
2. There are 2 user connected to the cluster sending & receiving messages via jetstream.
3. The account JWT is updated with total concurrent connections reduced from 5 to 1. Both the client disconnects & the 1 client reconnects successfully (this is great, this is what i want). But when i try to publish to jetstream, the library throws a 503 error. 
4. Now, even when i rerun the client as a new process, i'm still thrown with the 503 error.
5. After playing around a bit, i ran `nats stream cluster step-down` and reran the clients, it works. Even when i change the total concurrent connections (increase & then decrease the connection count)
6. When I increase the connection count to a higher number, there are no errors caught. 

Here is example code on how I update the account JWT
```
import { connect, JSONCodec, credsAuthenticator } from "nats";
import { fromPublic, fromSeed } from "nkeys.js";
import { encode, decode, createAccount, createUser, encodeUser, encodeAccount, Algorithms } from "nats-jwt";

var key = new TextEncoder().encode(secretKey);
    issuerKey = fromSeed(key);

    natsClient = await initNatsClient();

    var seed = "SAABDEQGDZAORB5ZMKEKGWIRPZKKETA5Q2K2A6T4UI74GHT6HQY3WXQCQE";

    //ACJ3KKXIRPLN7QYENN2LYWBMODGJW5JXGULTDTSF6XTQVF335YLHL7H5

    const nats = {
        "imports": [{
            "type": "service",
            "subject": "api.customer.*",
            "account": "AAPM4VH7H2OGUX2GXIMAXJUGWEI3AUHZXEFI25OQAA3OKE26DVDTLH4E" // System account public key
        }],
        "limits": {
            "subs": -1,
            "data": -1,
            "conn": 7,
            "payload": -1,
            "imports": -1,
            "wildcards": true,
            "mem_storage": -1,
            "disk_storage": -1,
            "streams": -1,
            "consumer": -1
        }
    }

    var accKeys = fromSeed(encoder.encode(seed));

    console.log(accKeys.getPublicKey())
    var accPublicKey = accKeys.getPublicKey();

    var operatorKey = await encodeAccount("test-org", accKeys, nats, {
        signer: issuerKey
    });

    operatorKey = new TextEncoder().encode(operatorKey);

    try {
        // Can be done only by a SYS account and no other account
        const message = await natsClient.request(`$SYS.REQ.CLAIMS.UPDATE`, operatorKey, {
            timeout: 5000
        });
        
        var response = JSON.parse(message.string());
        console.log(response);

        console.log("\n\n Account Updated!")
    } catch (err) {
        console.log(`problem with request: ${err.message}`);
        console.info(err);
    }
```

Response:
`{
  server: {
    name: 'na_2',
    host: '0.0.0.0',
    id: 'NDYTB6MKV64WS64KPUFH5MDHFWMCA65UZS53UMJMHORMFNFYB76ZSHFC',
    cluster: 'na_cluster',
    ver: '2.11.0',
    jetstream: true,
    flags: 7,
    seq: 12948,
    time: '2025-04-07T13:18:22.52989418Z'
  },
  data: {
    account: 'ACJ3KKXIRPLN7QYENN2LYWBMODGJW5JXGULTDTSF6XTQVF335YLHL7H5',
    code: 200,
    message: 'jwt updated'
  }
}`

### Expected behavior

It updates the account limits and the clients should be able to send & receive messages via jetstream without throwing the 503 error.

### Server and client version

Server: 2.11.0
Client: nats.js (v2.28.2), @nats-io/jetstream (3.0.0-35)

### Host environment

Server running on docker
Client running on macOS Sequoia 15.3.2

### Steps to reproduce

_No response_

Hints from issue comments:
@junpreetham: Hello? is anyone available to assist?@ripienaar: I can reproduce this:

5 connection Jetstream account:

```
$ nats auth account add LIMITED --connections 5 --jetstream
$ nats auth account push LIMITED
$ nats auth user add test LIMITED --credential limited_test_user.cred
```

Add a stream:

```
$ nats s add TEST --creds limited_test_user.cred --defaults --replicas 3 --subjects js.in.test
```

Start a publisher and subscriber:

```
$ nats pub js.in.test '{{ .Count }}' --count 100000 --sleep 1s --creds limited_test_user.cred -J
$ nats sub js.in.test --stream TEST --creds limited_test_user.cred --all
```

At this point every works, if we now update the account to 1 connection:

```
$ nats auth account edit LIMITED --connections 1
$ nats auth account push LIMITED
```

Immediately one of the connections will drop as expected and further jetstream publishes will either timeout or get no responders.

If we do a leader election on the stream it starts accepting messages again:

```
$ nats s cluster down TEST --creds limited_test_user.cred
```

~The behaviour seems specifically to setting connections to 1, setting to 2 would be fine~ The trigger is setting the connections to any value lower than currently connected clients.@junpreetham: Hey @ripienaar,

It's not specific to setting active connections from 2 to 1 but its specific to reducing the active connections from the current number to a number lesser than the total current client connections@ripienaar: @junpreetham yeah double checked now and you're correct, any number less than current connections.@neilalexander: Thanks for the report, this will be fixed in 2.10.28/2.11.2.@junpreetham: Thank you for the quick resolution gents. Any idea when this will be released? And when will the docker images be released?@neilalexander: I think we're expecting to put these versions out next week. If you want something earlier than that and you're happy with a nightly image, I can kick off a build right now.@junpreetham: If you can kick off a build now, that would be great!@neilalexander: Sure thing, give `synadia/nats-server:nightly-20250410` a try.@junpreetham: Still seeing the 503 issue unfortunately. When I execute the code above, i get a success message but the nats server logs throw the following error

[ERR] DirResolver - Update for account "ACJ3KKXIRPLN7QYENN2LYWBMODGJW5JXGULTDTSF6XTQVF335YLHL7H5" resulted in error expected 3 chunks

So when i try again, it works but it randomly seems to timeout and then fail with the 503@ripienaar: Make sure you pulled the latest nightly, I cant reproduce it with the correct binary.

Please show the server startup text
@junpreetham: ```
Attaching to accounts_server, auth, nats1, nats2, nats3, nats4, nats5, nats6, plans, projects, user_manager
nats5            | [1] 2025/04/10 13:09:49.698718 [INF] Starting nats-server
nats5            | [1] 2025/04/10 13:09:49.776603 [INF]   Version:  2.11.1-dev
nats1            | [1] 2025/04/10 13:09:49.764420 [INF] Starting nats-server
nats5            | [1] 2025/04/10 13:09:49.776674 [INF]   Git:      [b3a66b8]
nats1            | [1] 2025/04/10 13:09:49.777236 [INF]   Version:  2.11.1-dev
nats5            | [1] 2025/04/10 13:09:49.776704 [INF]   Cluster:  na_cluster
nats1            | [1] 2025/04/10 13:09:49.777321 [INF]   Git:      [b3a66b8]
nats5            | [1] 2025/04/10 13:09:49.776763 [INF]   Name:     na_5
nats1            | [1] 2025/04/10 13:09:49.777365 [INF]   Cluster:  na_cluster
nats5            | [1] 2025/04/10 13:09:49.776841 [INF]   Node:     0MmreNqu
nats1            | [1] 2025/04/10 13:09:49.777396 [INF]   Name:     na_1
nats5            | [1] 2025/04/10 13:09:49.776863 [INF]   ID:       NDTSEKFJJMAY4YYPXZ5A3Q3UDNJ2FNHDT2HTALDPAUXSM63HDYK2CAWH
nats1            | [1] 2025/04/10 13:09:49.777532 [INF]   Node:     kfIkPtd5
nats5            | [1] 2025/04/10 13:09:49.777593 [INF] Using configuration file: /etc/server.conf (sha256:01a62da262dab14eed1e3d86c43e8f2626ede78bb27a597665fafece4c90a070)
nats1            | [1] 2025/04/10 13:09:49.777596 [INF]   ID:       NBJHDATCPLI47VBDIPAOBKCQBKVE2YKAF2ONYHOMKICVQ56AIGF5TMMV
nats5            | [1] 2025/04/10 13:09:49.777729 [INF] Trusted Operators
nats1            | [1] 2025/04/10 13:09:49.778051 [INF] Using configuration file: /etc/server.conf (sha256:01a62da262dab14eed1e3d86c43e8f2626ede78bb27a597665fafece4c90a070)
nats5            | [1] 2025/04/10 13:09:49.777812 [INF]   System  : ""
nats3            | [1] 2025/04/10 13:09:49.782194 [INF] Starting nats-server
nats1            | [1] 2025/04/10 13:09:49.778086 [INF] Trusted Operators
nats5            | [1] 2025/04/10 13:09:49.778028 [INF]   Operator: "relay_local"
nats3            | [1] 2025/04/10 13:09:49.785047 [INF]   Version:  2.11.1-dev
nats1            | [1] 2025/04/10 13:09:49.778112 [INF]   System  : ""
nats5            | [1] 2025/04/10 13:09:49.778699 [INF]   Issued  : 2025-03-29 10:01:14 +0000 UTC
nats3            | [1] 2025/04/10 13:09:49.785174 [INF]   Git:      [b3a66b8]
nats1            | [1] 2025/04/10 13:09:49.778302 [INF]   Operator: "relay_local"
nats3            | [1] 2025/04/10 13:09:49.785199 [INF]   Cluster:  na_cluster
nats5            | [1] 2025/04/10 13:09:49.780389 [INF]   Expires : Never
nats1            | [1] 2025/04/10 13:09:49.779191 [INF]   Issued  : 2025-03-29 10:01:14 +0000 UTC
nats3            | [1] 2025/04/10 13:09:49.785220 [INF]   Name:     na_3
nats1            | [1] 2025/04/10 13:09:49.780805 [INF]   Expires : Never
nats3            | [1] 2025/04/10 13:09:49.785269 [INF]   Node:     636bMXaS
nats3            | [1] 2025/04/10 13:09:49.785292 [INF]   ID:       NCJOHT3KM34MIXVYUACHCR3IM4J7N2KQFAWTVH4JOQ7FE64HJ4AGCSRK
nats3            | [1] 2025/04/10 13:09:49.833159 [INF] Using configuration file: /etc/server.conf (sha256:01a62da262dab14eed1e3d86c43e8f2626ede78bb27a597665fafece4c90a070)
nats3            | [1] 2025/04/10 13:09:49.833355 [INF] Trusted Operators
nats3            | [1] 2025/04/10 13:09:49.833384 [INF]   System  : ""
nats3            | [1] 2025/04/10 13:09:49.833793 [INF]   Operator: "relay_local"
nats3            | [1] 2025/04/10 13:09:49.844806 [INF]   Issued  : 2025-03-29 10:01:14 +0000 UTC
nats3            | [1] 2025/04/10 13:09:49.851275 [INF]   Expires : Never
nats3            | [1] 2025/04/10 13:09:49.887662 [INF] Managing all jwt in exclusive directory /jwt
nats3            | [1] 2025/04/10 13:09:49.888251 [INF] Starting JetStream
nats5            | [1] 2025/04/10 13:09:49.887887 [INF] Managing all jwt in exclusive directory /jwt
nats5            | [1] 2025/04/10 13:09:49.888556 [INF] Starting JetStream
nats3            | [1] 2025/04/10 13:09:49.889520 [WRN] Temporary storage directory used, data could be lost on system reboot
nats1            | [1] 2025/04/10 13:09:49.897104 [INF] Managing all jwt in exclusive directory /jwt
nats1            | [1] 2025/04/10 13:09:49.898524 [INF] Starting JetStream
nats1            | [1] 2025/04/10 13:09:49.899568 [WRN] Temporary storage directory used, data could be lost on system reboot
nats5            | [1] 2025/04/10 13:09:49.890772 [WRN] Temporary storage directory used, data could be lost on system reboot
nats4            | [1] 2025/04/10 13:09:49.786432 [INF] Starting nats-server
nats4            | [1] 2025/04/10 13:09:49.885120 [INF]   Version:  2.11.1-dev
nats4            | [1] 2025/04/10 13:09:49.885165 [INF]   Git:      [b3a66b8]
nats4            | [1] 2025/04/10 13:09:49.885181 [INF]   Cluster:  na_cluster
nats4            | [1] 2025/04/10 13:09:49.885203 [INF]   Name:     na_4
nats4            | [1] 2025/04/10 13:09:49.885272 [INF]   Node:     4ANwidLa
nats4            | [1] 2025/04/10 13:09:49.885291 [INF]   ID:       NABT2X5C5UWKCJH7PEZ3SS7JGRO57TUQPGBEMMAPCS23FANJNTSZO5SL
nats4            | [1] 2025/04/10 13:09:49.885592 [INF] Using configuration file: /etc/server.conf (sha256:01a62da262dab14eed1e3d86c43e8f2626ede78bb27a597665fafece4c90a070)
nats4            | [1] 2025/04/10 13:09:49.885623 [INF] Trusted Operators
nats4            | [1] 2025/04/10 13:09:49.885648 [INF]   System  : ""
nats4            | [1] 2025/04/10 13:09:49.885808 [INF]   Operator: "relay_local"
nats4            | [1] 2025/04/10 13:09:49.886393 [INF]   Issued  : 2025-03-29 10:01:14 +0000 UTC
nats4            | [1] 2025/04/10 13:09:49.887803 [INF]   Expires : Never
nats5            | [1] 2025/04/10 13:09:49.950713 [INF]     _ ___ _____ ___ _____ ___ ___   _   __  __
nats5            | [1] 2025/04/10 13:09:49.950886 [INF]  _ | | __|_   _/ __|_   _| _ \ __| /_\ |  \/  |
nats5            | [1] 2025/04/10 13:09:49.950936 [INF] | || | _|  | | \__ \ | | |   / _| / _ \| |\/| |
nats5            | [1] 2025/04/10 13:09:49.951141 [INF]  \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_|  |_|
nats5            | [1] 2025/04/10 13:09:49.951270 [INF] 
nats5            | [1] 2025/04/10 13:09:49.951327 [INF]          https://docs.nats.io/jetstream
nats5            | [1] 2025/04/10 13:09:49.951521 [INF] 
nats5            | [1] 2025/04/10 13:09:49.951638 [INF] ---------------- JETSTREAM ----------------
nats5            | [1] 2025/04/10 13:09:49.951832 [INF]   Max Memory:      2.87 GB
nats5            | [1] 2025/04/10 13:09:49.951880 [INF]   Max Storage:     23.89 GB
nats5            | [1] 2025/04/10 13:09:49.952028 [INF]   Store Directory: "/tmp/nats/jetstream"
nats5            | [1] 2025/04/10 13:09:49.952082 [INF]   API Level:       1
nats5            | [1] 2025/04/10 13:09:49.952105 [INF] -------------------------------------------
nats3            | [1] 2025/04/10 13:09:49.951036 [INF]     _ ___ _____ ___ _____ ___ ___   _   __  __
nats3            | [1] 2025/04/10 13:09:49.951066 [INF]  _ | | __|_   _/ __|_   _| _ \ __| /_\ |  \/  |
nats3            | [1] 2025/04/10 13:09:49.951079 [INF] | || | _|  | | \__ \ | | |   / _| / _ \| |\/| |
nats3            | [1] 2025/04/10 13:09:49.951088 [INF]  \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_|  |_|
nats3            | [1] 2025/04/10 13:09:49.951096 [INF] 
nats3            | [1] 2025/04/10 13:09:49.951104 [INF]          https://docs.nats.io/jetstream
nats3            | [1] 2025/04/10 13:09:49.951112 [INF] 
nats3            | [1] 2025/04/10 13:09:49.951125 [INF] ---------------- JETSTREAM ----------------
nats3            | [1] 2025/04/10 13:09:49.951152 [INF]   Max Memory:      2.87 GB
nats3            | [1] 2025/04/10 13:09:49.951166 [INF]   Max Storage:     23.89 GB
nats3            | [1] 2025/04/10 13:09:49.951174 [INF]   Store Directory: "/tmp/nats/jetstream"
nats3            | [1] 2025/04/10 13:09:49.951192 [INF]   API Level:       1
nats3            | [1] 2025/04/10 13:09:49.951218 [INF] -------------------------------------------
nats1            | [1] 2025/04/10 13:09:49.970608 [INF]     _ ___ _____ ___ _____ ___ ___   _   __  __
nats1            | [1] 2025/04/10 13:09:49.970726 [INF]  _ | | __|_   _/ __|_   _| _ \ __| /_\ |  \/  |
nats4            | [1] 2025/04/10 13:09:49.971409 [INF] Managing all jwt in exclusive directory /jwt
nats4            | [1] 2025/04/10 13:09:49.972262 [INF] Starting JetStream
nats4            | [1] 2025/04/10 13:09:49.972401 [WRN] Temporary storage directory used, data could be lost on system reboot
nats5            | [1] 2025/04/10 13:09:50.005580 [INF] Starting JetStream cluster
nats5            | [1] 2025/04/10 13:09:50.006046 [INF] Creating JetStream metadata controller
nats1            | [1] 2025/04/10 13:09:50.006008 [INF] | || | _|  | | \__ \ | | |   / _| / _ \| |\/| |
nats1            | [1] 2025/04/10 13:09:50.006041 [INF]  \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_|  |_|
nats1            | [1] 2025/04/10 13:09:50.006050 [INF] 
nats1            | [1] 2025/04/10 13:09:50.006057 [INF]          https://docs.nats.io/jetstream
nats1            | [1] 2025/04/10 13:09:50.006065 [INF] 
nats1            | [1] 2025/04/10 13:09:50.006073 [INF] ---------------- JETSTREAM ----------------
nats1            | [1] 2025/04/10 13:09:50.006098 [INF]   Max Memory:      2.87 GB
nats1            | [1] 2025/04/10 13:09:50.006123 [INF]   Max Storage:     23.89 GB
nats1            | [1] 2025/04/10 13:09:50.006139 [INF]   Store Directory: "/tmp/nats/jetstream"
nats1            | [1] 2025/04/10 13:09:50.006187 [INF]   API Level:       1
nats1            | [1] 2025/04/10 13:09:50.006200 [INF] -------------------------------------------
nats4            | [1] 2025/04/10 13:09:50.022458 [INF]     _ ___ _____ ___ _____ ___ ___   _   __  __
nats4            | [1] 2025/04/10 13:09:50.022485 [INF]  _ | | __|_   _/ __|_   _| _ \ __| /_\ |  \/  |
nats4            | [1] 2025/04/10 13:09:50.022496 [INF] | || | _|  | | \__ \ | | |   / _| / _ \| |\/| |
nats4            | [1] 2025/04/10 13:09:50.022503 [INF]  \__/|___| |_| |___/ |_| |_|_\___/_/ \_\_|  |_|
nats4            | [1] 2025/04/10 13:09:50.022510 [INF] 
nats4            | [1] 2025/04/10 13:09:50.022518 [INF]          https://docs.nats.io/jetstream
nats4            | [1] 2025/04/10 13:09:50.022526 [INF] 
nats4            | [1] 2025/04/10 13:09:50.022543 [INF] ---------------- JETSTREAM ----------------
nats4            | [1] 2025/04/10 13:09:50.022570 [INF]   Max Memory:      2.87 GB
nats4            | [1] 2025/04/10 13:09:50.022582 [INF]   Max Storage:     23.89 GB
nats4            | [1] 2025/04/10 13:09:50.022594 [INF]   Store Directory: "/tmp/nats/jetstream"
nats4            | [1] 2025/04/10 13:09:50.022608 [INF]   API Level:       1
nats4            | [1] 2025/04/10 13:09:50.022620 [INF] -------------------------------------------
```@ripienaar: Does seem to be the right one. The expected 3 chunks, seen that with bad JWTs.  For me using `nats auth` I cant reproduce anymore@junpreetham: What do you define as a bad JWT?@ripienaar: Badly formed - JWTs are a string when split on `.` yields 3 chunks.  But there could be other reasons too of course@junpreetham: Yeah in terms of the split, i'm able to generate 3 chunks but the error still pops up in the server logs but response indicates success@junpreetham: Example JWT which is "bad" => eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJhdWQiOiJOQVRTIiwibmFtZSI6IjY3Zjc3ZDA5MjA1ZWM5NWZhNjM3MTFhZiIsInN1YiI6IkFBUlNTSDdQVjVXTTJTVERKNFBUVzNBNExCTUxWMjJPUjZVSkVHTTJOQzdCT1lPTkJPVzVBWVRZIiwibmF0cyI6eyJpbXBvcnRzIjpbeyJ0eXBlIjoic2VydmljZSIsInN1YmplY3QiOiJhY2NvdW50cy51c2VyLioiLCJhY2NvdW50IjoiQUFQTTRWSDdIMk9HVVgyR1hJTUFYSlVHV0VJM0FVSFpYRUZJMjVPUUFBM09LRTI2RFZEVExINEUifV0sImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsImNvbm4iOjUsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwid2lsZGNhcmRzIjp0cnVlLCJtZW1fc3RvcmFnZSI6LTEsImRpc2tfc3RvcmFnZSI6LTEsInN0cmVhbXMiOi0xLCJjb25zdW1lciI6LTF9LCJ0eXBlIjoiYWNjb3VudCIsInZlcnNpb24iOjJ9LCJpc3MiOiJPQlFDRlVPUlpWRldaRkNWUDVUNlA1TVFRUjZSQ05FVDIzNEJJUVdEWkFVR09JWEhRTldCQk1MWCIsImlhdCI6MTc0NDI5MjA3NiwianRpIjoiT1lrTlhEeDdMcGt4UXNmWmFDd1R6SzBMR1BTbWxRYTNLdjFvcHliMitTUmEwcTljbjYwZkp0dG9OaHVoVlFWc09CVGtzeHZJZXBWc1Y3NFYwdzYvdkE9PSJ9.l8WaVTKbxiUEKpFIVwT_v2_bknbXbjoXjcCCDfCyD53waKPaHOYlqZiv-NH7B_J4Ec9NdRhcmP0Zi9e22hm0Dw

Solution patch:
diff --git a/server/accounts.go b/server/accounts.go
index 44afc237..72f6a8c6 100644
--- a/server/accounts.go
+++ b/server/accounts.go
@@ -3801,13 +3801,18 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
 		}
 	}
 
-	for i, c := range clients {
+	count := 0
+	for _, c := range clients {
 		a.mu.RLock()
-		exceeded := a.mconns != jwt.NoLimit && i >= int(a.mconns)
+		exceeded := a.mconns != jwt.NoLimit && count >= int(a.mconns)
 		a.mu.RUnlock()
-		if exceeded {
-			c.maxAccountConnExceeded()
-			continue
+		// Only kick non-internal clients.
+		if !isInternalClient(c.kind) {
+			if exceeded {
+				c.maxAccountConnExceeded()
+				continue
+			}
+			count++
 		}
 		c.mu.Lock()
 		c.applyAccountLimits()
diff --git a/server/client.go b/server/client.go
index 82b8caf4..64c0c3b9 100644
--- a/server/client.go
+++ b/server/client.go
@@ -56,6 +56,11 @@ const (
 	ACCOUNT
 )
 
+// Internal clients. kind should be SYSTEM, JETSTREAM or ACCOUNT
+func isInternalClient(kind int) bool {
+	return kind == SYSTEM || kind == JETSTREAM || kind == ACCOUNT
+}
+
 // Extended type of a CLIENT connection. This is returned by c.clientType()
 // and indicate what type of client connection we are dealing with.
 // If invoked on a non CLIENT connection, NON_CLIENT type is returned.
@@ -2782,7 +2787,7 @@ func (c *client) processSubEx(subject, queue, bsid []byte, cb msgHandler, noForw
 
 	// This check does not apply to SYSTEM or JETSTREAM or ACCOUNT clients (because they don't have a `nc`...)
 	// When a connection is closed though, we set c.subs to nil. So check for the map to not be nil.
-	if (c.isClosed() && (kind != SYSTEM && kind != JETSTREAM && kind != ACCOUNT)) || (c.subs == nil) {
+	if (c.isClosed() && !isInternalClient(kind)) || (c.subs == nil) {
 		c.mu.Unlock()
 		return nil, ErrConnectionClosed
 	}
@@ -4680,9 +4685,7 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
 	// Check for JetStream encoded reply subjects.
 	// For now these will only be on $JS.ACK prefixed reply subjects.
 	var remapped bool
-	if len(creply) > 0 &&
-		c.kind != CLIENT && c.kind != SYSTEM && c.kind != JETSTREAM && c.kind != ACCOUNT &&
-		bytes.HasPrefix(creply, []byte(jsAckPre)) {
+	if len(creply) > 0 && c.kind != CLIENT && !isInternalClient(c.kind) && bytes.HasPrefix(creply, []byte(jsAckPre)) {
 		// We need to rewrite the subject and the reply.
 		// But, we must be careful that the stream name, consumer name, and subject can contain '@' characters.
 		// JS ACK contains at least 8 dots, find the first @ after this prefix.
diff --git a/server/filestore.go b/server/filestore.go
index 710d55d0..8809d0ff 100644
--- a/server/filestore.go
+++ b/server/filestore.go
@@ -5572,7 +5572,7 @@ func (fs *fileStore) expireMsgs() {
 					return false
 				}
 			} else {
-				fs.removeMsgViaLimits(seq)
+				fs.removeMsg(seq, false, false, false)
 			}
 			return true
 		})
diff --git a/server/server.go b/server/server.go
index 8b688468..ce878d74 100644
--- a/server/server.go
+++ b/server/server.go
@@ -1837,9 +1837,9 @@ func (s *Server) createInternalAccountClient() *client {
 	return s.createInternalClient(ACCOUNT)
 }
 
-// Internal clients. kind should be SYSTEM or JETSTREAM
+// Internal clients. kind should be SYSTEM, JETSTREAM or ACCOUNT
 func (s *Server) createInternalClient(kind int) *client {
-	if kind != SYSTEM && kind != JETSTREAM && kind != ACCOUNT {
+	if !isInternalClient(kind) {
 		return nil
 	}
 	now := time.Now()



Test patch:
diff --git a/server/filestore_test.go b/server/filestore_test.go
index a0af0603..a0c68e5b 100644
--- a/server/filestore_test.go
+++ b/server/filestore_test.go
@@ -8580,6 +8580,68 @@ func TestFileStoreMessageTTLRecoveredSingleMessageWithoutStreamState(t *testing.
 	})
 }
 
+func TestFileStoreMessageTTLWriteTombstone(t *testing.T) {
+	s := RunBasicJetStreamServer(t)
+	defer s.Shutdown()
+
+	dir := t.TempDir()
+
+	fs, err := newFileStore(
+		FileStoreConfig{StoreDir: dir, srv: s},
+		StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
+	require_NoError(t, err)
+	defer fs.Stop()
+
+	ttl := int64(1)
+
+	// When the timed hash wheel state is deleted, the only way we can recover
+	// the TTL is to look at the original message header, therefore the TTL
+	// must be in the headers for this test to work.
+	hdr := fmt.Appendf(nil, "NATS/1.0\r\n%s: %d\r\n", JSMessageTTL, ttl)
+	_, _, err = fs.StoreMsg("test", hdr, nil, ttl)
+	require_NoError(t, err)
+
+	// Publish another message, but without TTL.
+	_, _, err = fs.StoreMsg("test", nil, nil, 0)
+	require_NoError(t, err)
+
+	var ss StreamState
+	fs.FastState(&ss)
+	require_Equal(t, ss.Msgs, 2)
+	require_Equal(t, ss.FirstSeq, 1)
+	require_Equal(t, ss.LastSeq, 2)
+
+	checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
+		if fs.State().FirstSeq == 1 {
+			return errors.New("message not expired yet")
+		}
+		return nil
+	})
+
+	fs.FastState(&ss)
+	require_Equal(t, ss.Msgs, 1)
+	require_Equal(t, ss.FirstSeq, 2)
+	require_Equal(t, ss.LastSeq, 2)
+
+	fs.Stop()
+
+	// Delete the stream state file so that we need to rebuild.
+	// Should have written a tombstone so we can properly recover.
+	fn := filepath.Join(dir, msgDir, streamStreamStateFile)
+	require_NoError(t, os.RemoveAll(fn))
+
+	fs, err = newFileStore(
+		FileStoreConfig{StoreDir: dir, srv: s},
+		StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
+	require_NoError(t, err)
+	defer fs.Stop()
+
+	fs.FastState(&ss)
+	require_Equal(t, ss.Msgs, 1)
+	require_Equal(t, ss.FirstSeq, 2)
+	require_Equal(t, ss.LastSeq, 2)
+}
+
 func TestFileStoreDontSpamCompactWhenMostlyTombstones(t *testing.T) {
 	fs, err := newFileStore(
 		FileStoreConfig{StoreDir: t.TempDir(), BlockSize: defaultMediumBlockSize},
diff --git a/server/jwt_test.go b/server/jwt_test.go
index 97f224b2..3e1de983 100644
--- a/server/jwt_test.go
+++ b/server/jwt_test.go
@@ -7221,3 +7221,63 @@ func TestJWTUpdateAccountClaimsStreamAndServiceImportDeadlock(t *testing.T) {
 		})
 	}
 }
+
+func TestJWTJetStreamClientsExcludedForMaxConnsUpdate(t *testing.T) {
+	sysKp, syspub := createKey(t)
+	sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub)
+	sysCreds := newUser(t, sysKp)
+
+	accKp, accPub := createKey(t)
+	accClaim := jwt.NewAccountClaims(accPub)
+	accClaim.Name = "acc"
+	accClaim.Limits.JetStreamTieredLimits["R1"] = jwt.JetStreamLimits{
+		DiskStorage: 1100, MemoryStorage: 0, Consumer: 2, Streams: 2}
+	accClaim.Limits.Conn = 5
+	accJwt1 := encodeClaim(t, accClaim, accPub)
+	accCreds := newUser(t, accKp)
+
+	storeDir := t.TempDir()
+
+	dirSrv := t.TempDir()
+	cf := createConfFile(t, []byte(fmt.Sprintf(`
+		listen: 127.0.0.1:-1
+		server_name: s1
+		jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
+		leaf {
+			listen: 127.0.0.1:-1
+		}
+		operator: %s
+		system_account: %s
+		resolver: {
+			type: full
+			dir: '%s'
+		}
+	`, storeDir, ojwt, syspub, dirSrv)))
+
+	s, _ := RunServerWithConfig(cf)
+	defer s.Shutdown()
+
+	updateJwt(t, s.ClientURL(), sysCreds, sysJwt, 1)
+	updateJwt(t, s.ClientURL(), sysCreds, accJwt1, 1)
+
+	nc, js := jsClientConnectURL(t, s.ClientURL(), nats.UserCredentials(accCreds))
+	defer nc.Close()
+
+	_, err := js.AddStream(&nats.StreamConfig{Name: "TEST", Replicas: 1, Subjects: []string{"foo"}})
+	require_NoError(t, err)
+
+	_, err = js.Publish("foo", nil)
+	require_NoError(t, err)
+
+	accClaim.Limits.Conn = 1
+	accJwt1 = encodeClaim(t, accClaim, accPub)
+	updateJwt(t, s.ClientURL(), sysCreds, accJwt1, 1)
+
+	// Manually reconnect.
+	nc.Close()
+	nc, js = jsClientConnectURL(t, s.ClientURL(), nats.UserCredentials(accCreds))
+	defer nc.Close()
+
+	_, err = js.Publish("foo", nil)
+	require_NoError(t, err)
+}