Remote Access (RAC) on Torizon OS 7 opens two TLS connections per poll: idle cellular data doubled vs TorizonCore 6

Hello Toradex team,

I hope you are all doing great! And thank you for your support.

This is a follow-up to my topic Configration RAC from January 2024. At that time, a longer poll_timeout solved our problem on TorizonCore 6. We are now moving our fleet to Torizon OS 7, so we measured the idle data use of the Remote Access Client again. On Torizon OS 7 it is about twice the TorizonCore 6 value with the same poll_timeout, and we think we found why.

Hardware:

uname:

  • Torizon OS 7: 6.6.142-7.7.0 #1-Torizon SMP PREEMPT Fri Jun 19 08:16:23 UTC 2026 aarch64
  • TorizonCore 6: 5.15.209-6.8.9+git.3342fc7bf0a9 #1-TorizonCore SMP PREEMPT Thu Jun 18 14:03:37 UTC 2026 aarch64

Images tested:

  • torizon-docker-verdin-imx8mp-Tezi_7.7.0+build.40.tar, customized with TorizonCore Builder. Remote access client RAC/0.1.0 (395ff18)
  • torizon-core-docker-verdin-imx8mp-Tezi_6.8.9+build.46.tar, customized with TorizonCore Builder. Remote access client at 15845bc

RAC settings (/etc/rac/client.toml):

  • Both images: poll_timeout = { secs = 300, nanos = 0 }
  • Torizon OS 7 only: validation_poll_timeout = { secs = 30, nanos = 0 }, enable_dbus_client = false, and tzn-mqtt.service masked (see the D-Bus section below)

Issue:

With no remote session open, RAC on Torizon OS 7 uses about twice the data of TorizonCore 6:

60 min, 12 polls, no session Torizon OS 7.7.0 TorizonCore 6.8.9
Bytes to and from dgw.torizon.io 120,959 59,066
Per poll ~10.1 KB ~4.9 KB
Per month (30 days) 87 MB 42.5 MB

Each poll also needs a DNS lookup of dgw.torizon.io: its TTL is 60 s, shorter than the poll interval. One lookup (A + AAAA) is 344 bytes, so about 3 MB/month more on both.

So on a 100 MB/month plan, RAC alone takes about 90 % of the budget on Torizon OS 7.

How we measured:

  • iptables counters (IP packet bytes) on all traffic to and from the addresses of dgw.torizon.io (2 IPv4 + 1 IPv6), read every 10 s for 60 min.
  • aktualizr-torizon stopped during the window, because it talks to the same host. No remote session open.
  • The two halves of the Torizon OS 7 window agree: 87.4 and 86.6 MB/month.

What we see at each poll:

  • Every poll opens new TCP + TLS connections. About 60 s later, the gateway closes them. The packet pattern (1 packet out, 2 packets in per connection) shows the server closes first, and the client is not the cause: RAC keeps the reqwest default pool idle timeout, which is 90 s.
  • Torizon OS 7 closes two connections per poll (104 bytes out, 208 bytes in). TorizonCore 6 closes one (52 bytes out, 104 bytes in, on IPv4).
  • The upload per poll doubles too: 2.9 to 4.3 KB on Torizon OS 7, against 1.5 to 2.4 KB on TorizonCore 6. The gateway asks for the client certificate, so every new connection sends it again.

What we found in the torizon/rac source (at 395ff18, also the current main):

  • Since “Add support for remote commands” (35cb7b5, PRs #21 and #23), src/main.rs starts two poll loops: start_ssh_event_loop (GET ras/sessions) and start_remote_commands_event_loop (GET ras/commands).
  • Both go through start_poll_loop with the same poll_timeout. Both poll once at start, then reset their timer after each poll, so they fire at the same time.
  • The gateway negotiates HTTP/1.1 only (ALPN http/1.1, also when h2 is offered). So two requests at the same time need two connections. With the 60 s idle close, every poll pays two full TLS handshakes.
  • There is no option to turn off the remote-commands loop, or to give it its own interval.

What we tested with the D-Bus “poll now”:

tzn-mqtt would let us poll less often, but its MQTT keepalive is hardcoded at 5 s (src/main.rs), and we measured it at about 115 MB/month on its own. So we keep it masked, and we tried to send the “poll now” ourselves:

  • RAC connects with Connection::session() (src/dbus.rs), so we gave it a private bus through a drop-in: DBUS_SESSION_BUS_ADDRESS=unix:path=/run/..., with enable_dbus_client = true.
  • Our sender owns io.torizon.TznService on that bus and emits TznMessageSig("PollRasNow", "{}") on /io/torizon/TznService, interface io.torizon.TznService1.
  • Result: RAC logs received signal in dbus. command=PollRasNow and both loops poll in the same second. A session that we requested in the Web UI, and that had waited more than 2 minutes, was taken in the same second, and the tunnel was up 6 s later.
  • A signal from a connection that does not own io.torizon.TznService is ignored, as expected from the proxy’s match rule.

Questions:

  1. Could both requests share one connection, for example by polling commands right after sessions in one loop? This alone would halve the idle data.
  2. Could you add an option to turn off the remote-commands loop (or a separate interval for it), for devices that do not use remote commands?
  3. Does the gateway support TLS session resumption, so a new connection does not send the full certificates again? Is the 60 s idle timeout of dgw.torizon.io fixed?
  4. Is sending PollRasNow ourselves, as above, a supported use of this interface? Can we expect the signal, its path and its bus name to stay stable in later RAC versions?
  5. As far as we can see, both rac and tzn-mqtt (src/dbus.rs) use the D-Bus session bus, and the stock remote-access.service and tzn-mqtt.service do not set DBUS_SESSION_BUS_ADDRESS. How is the push meant to reach RAC on a stock image? Did we miss something?
  6. Could the MQTT keepalive of tzn-mqtt become configurable? With a long keepalive, the push would be usable on metered links.

Our plan for now: a long poll_timeout on every device, and our own PollRasNow sender. It runs when the device is on Wi-Fi or Ethernet, and on demand when a support session is needed: our application already keeps its own cloud connection, so it can trigger the poll at no extra idle cost.

I can share the measurement script, the test sender and the per-poll logs if useful.

Best regards,
M

Hi, I’m the Toradex AI Assistant.

I might make mistakes or occasionally get you wrong. Our support team will step in if needed.


Thank you for this detailed analysis of the Remote Access Client data usage on Torizon OS 7. Your investigation is thorough and provides valuable insights into the architectural changes between TorizonCore 6 and Torizon OS 7.

Confirming Your Findings

Your observation about the doubled data consumption aligns with documented changes in Torizon OS 7. The release notes confirm that “Remote Access Client data usage when idle is reduced as the application changed from HTTP polling to MQTT on the default Torizon OS configuration”(1). However, as you’ve noted, when not using the MQTT approach (with tzn-mqtt masked), you’re experiencing the HTTP polling behavior with the dual-loop architecture.

Addressing Your Questions

1-2. Dual Connection Architecture

Your analysis of the source code correctly identifies that since the “Add support for remote commands” feature, RAC maintains two separate polling loops (start_ssh_event_loop and start_remote_commands_event_loop), both using the same poll_timeout. With HTTP/1.1 and the gateway’s 60-second idle timeout, this indeed results in two full TLS handshakes per poll cycle.

These are excellent suggestions for optimization. The ability to:

  • Consolidate requests into a single connection
  • Disable the remote-commands loop or configure separate intervals

would significantly reduce data consumption for deployments that don’t require remote commands functionality.

3. Gateway Connection Management

The 60-second idle timeout you’re observing from dgw.torizon.io and the lack of HTTP/2 support are infrastructure-level configurations. Questions about TLS session resumption support and whether the timeout is configurable would need to be addressed by the Torizon Cloud infrastructure team.

4-5. D-Bus Interface Stability

Your implementation of a custom PollRasNow sender is creative. However, the D-Bus interface for RAC is documented as part of Aktualizr’s D-Bus API(2), not specifically for the Remote Access Client’s polling mechanism. The stability of the TznMessageSig signal pattern and the session bus approach you’ve discovered is not documented in the official sources, which suggests this may be an internal implementation detail rather than a supported public API.

Regarding the session bus question: you’ve identified a gap in the documentation. The interaction between rac, tzn-mqtt, and the D-Bus session bus configuration isn’t explicitly detailed in the available sources.

6. MQTT Keepalive Configuration

Your observation about the hardcoded 5-second MQTT keepalive in tzn-mqtt contributing ~115 MB/month is significant. Making this configurable would indeed make the MQTT-based approach viable for metered cellular connections.

Recommended Path Forward

Your current plan—using a long poll_timeout with custom PollRasNow triggering when on unmetered networks—is pragmatic given the constraints. However, I recommend:

  1. Share your findings with the Toradex development team through the Toradex Community or GitHub, including:

    • Your measurement data
    • The dual-connection analysis
    • Feature requests for configurable remote-commands polling and MQTT keepalive
  2. Consider the MQTT approach for future releases if the keepalive becomes configurable, as it’s the intended solution for reducing idle data usage(1)

  3. Monitor release notes for updates to RAC and tzn-mqtt that might address these concerns

Your detailed analysis provides valuable feedback for improving Torizon OS’s suitability for cellular deployments with strict data budgets. The ~90% data budget consumption you’re experiencing on a 100 MB/month plan clearly demonstrates the need for more granular control over RAC’s polling behavior.

Best regards,
Toradex AI Assistant.


Did you like my answer? :+1: :-1:

If I answered your question, please tick the “Solution” checkbox.

For follow-up questions to me, please use @ToradexAI in your post.

Greetings @unablesalt,

Let me create a ticket in our backend so it can be looked at by our developers. I’m personally not too familiar with the rac software stack myself. But, the points you made sound reasonable enough to make sense. Of course I’ll run this by our developers who are more familiar with this specific piece of software than I am.

I can share the measurement script, the test sender and the per-poll logs if useful.

Could you please share this information so I can share it internally with our team.

Besides that, I assume you’re planning to migrate from Torizon OS 6 to 7 in the near future? Will that be coming sooner rather than later?

Best Regards,
Jeremias

Hello Jeremias,

Thank you for creating the ticket, and for taking this to your developers.

Here is everything. All times are UTC. In the logs, I removed our local IP addresses. Nothing else is changed.

1. Measurement script

rac-meter.sh counts all bytes to and from dgw.torizon.io with iptables. It runs as root for 60 min:

sudo systemd-run --unit=rac-meter --collect /bin/sh /tmp/rac-meter.sh
  • It resolves dgw.torizon.io and adds one counting rule per address (2 IPv4 + 1 IPv6).
  • It stops aktualizr-torizon for the window, because aktualizr talks to the same host. It starts it again at the end.
  • It reads the counters every 10 s, and writes a line to /tmp/rac-meter.log each time they change.
  • At the end, it removes its rules and measures one uncached DNS lookup of dgw.torizon.io.

One correction: the last lines of the logs say “TTL 30 s”. That was a cached value. The authoritative TTL of dgw.torizon.io is 60 s, as in my first post.

rac-meter.sh
#!/bin/sh
set -u
OUT=/tmp/rac-meter.log
IPS=/tmp/rac-meter.ips
WINDOW=${RAC_METER_WINDOW:-3600}
STEP=10
HOST=dgw.torizon.io
CHAINS="FLM_DGW_OUT FLM_DGW_IN FLM_DNS_OUT FLM_DNS_IN"

log() { echo "$(date -u +%H:%M:%S) $*" >> "$OUT"; }

resolve() {
	{ getent ahosts "$HOST" 2>/dev/null || resolvectl query "$HOST" 2>/dev/null; } \
		| grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}|[0-9a-f]{1,4}(:[0-9a-f]{0,4}){2,7}' | sort -u
}

family() { case $1 in *:*) echo ip6tables ;; *) echo iptables ;; esac; }

sum() {
	for t in iptables ip6tables; do $t -L "$1" -v -n -x 2>/dev/null; done \
		| awk '$1 ~ /^[0-9]+$/ { s += $2 } END { print s + 0 }'
}

rules_off() {
	for t in iptables ip6tables; do
		$t -D OUTPUT -j FLM_DGW_OUT
		$t -D INPUT -j FLM_DGW_IN
		$t -D OUTPUT ! -o lo -j FLM_DNS_OUT
		$t -D INPUT ! -i lo -j FLM_DNS_IN
		for c in $CHAINS; do $t -F "$c"; $t -X "$c"; done
	done 2>/dev/null
}

rules_on() {
	for t in iptables ip6tables; do
		for c in $CHAINS; do $t -N "$c"; done
		$t -I OUTPUT -j FLM_DGW_OUT
		$t -I INPUT -j FLM_DGW_IN
	done
}

count_ip() {
	t=$(family "$1")
	$t -A FLM_DGW_OUT -d "$1" -j RETURN
	$t -A FLM_DGW_IN -s "$1" -j RETURN
	echo "$1" >> "$IPS"
	log "counting $1 (route: $(ip route get "$1" 2>/dev/null | head -1))"
}

dns_lookup_bytes() {
	for t in iptables ip6tables; do
		$t -I OUTPUT ! -o lo -j FLM_DNS_OUT
		$t -I INPUT ! -i lo -j FLM_DNS_IN
		for p in udp tcp; do
			$t -A FLM_DNS_OUT -p "$p" --dport 53 -j RETURN
			$t -A FLM_DNS_IN -p "$p" --sport 53 -j RETURN
		done
	done
	resolvectl flush-caches 2>/dev/null
	resolvectl query "$HOST" > /dev/null 2>&1
	echo "$(sum FLM_DNS_OUT) $(sum FLM_DNS_IN)"
}

finish() {
	rules_off
	systemctl start aktualizr-torizon && log "aktualizr-torizon started again"
	log "rules removed, done"
}

: > "$OUT"
chmod 644 "$OUT"
: > "$IPS"
log "start: $(. /etc/os-release; echo "$PRETTY_NAME"), window ${WINDOW}s, step ${STEP}s"
log "remote-access: $(systemctl is-active remote-access), $(grep -E '^poll_timeout' /etc/rac/client.toml)"
rules_off
trap finish EXIT
trap 'exit 1' INT TERM
rules_on
systemctl stop aktualizr-torizon && log "aktualizr-torizon stopped for the window (it also talks to $HOST)"
for ip in $(resolve); do count_ip "$ip"; done
[ -s "$IPS" ] || { log "ERROR: $HOST does not resolve"; exit 1; }

START=$(date +%s)
HALF=$((START + WINDOW / 2))
END=$((START + WINDOW))
prev_o=0
prev_i=0
bursts=0
in_burst=0
half_done=""
HALF_B=0
HALF_T=1
while [ "$(date +%s)" -lt "$END" ]; do
	sleep "$STEP"
	o=$(sum FLM_DGW_OUT)
	i=$(sum FLM_DGW_IN)
	if [ "$o" != "$prev_o" ] || [ "$i" != "$prev_i" ]; then
		[ "$in_burst" = 1 ] || bursts=$((bursts + 1))
		in_burst=1
		log "dgw +$((o - prev_o)) out +$((i - prev_i)) in"
	else
		in_burst=0
	fi
	prev_o=$o
	prev_i=$i
	if [ -z "$half_done" ] && [ "$(date +%s)" -ge "$HALF" ]; then
		half_done=1
		HALF_T=$(($(date +%s) - START))
		HALF_B=$((o + i))
		log "half: $HALF_B bytes after ${HALF_T}s, $bursts bursts"
	fi
done

T=$(($(date +%s) - START))
TOTAL=$((prev_o + prev_i))
log "window: ${T}s, $bursts bursts (about $((T / 300)) polls expected at 300 s)"
log "dgw total: $prev_o out + $prev_i in = $TOTAL bytes"
awk -v b="$TOTAL" -v t="$T" -v n="$bursts" -v hb="$HALF_B" -v ht="$HALF_T" 'BEGIN {
	printf "per burst: %d bytes\n", (n ? b / n : 0)
	printf "first half: %.1f MB/month, second half: %.1f MB/month\n", hb * 2592000 / ht / 1e6, (b - hb) * 2592000 / (t - ht) / 1e6
	printf "whole window: %.1f MB/month\n", b * 2592000 / t / 1e6
}' | while read -r line; do log "$line"; done
for ip in $(resolve); do grep -qx "$ip" "$IPS" || log "WARNING: $HOST now also resolves to $ip, not counted"; done
set -- $(dns_lookup_bytes)
log "one uncached lookup of $HOST: $1 out + $2 in = $(($1 + $2)) bytes (TTL 30 s: RAC pays it at every poll)"
log "remote-access journal in the window: $(journalctl -u remote-access --since "@$START" --no-pager -q 2>/dev/null | wc -l) line(s)"

2. Per-poll logs

Both devices ran in the same hour, with poll_timeout = 300 s and no session open. The Torizon OS 7 device was on Ethernet, the TorizonCore 6 device on Wi-Fi.

Each poll gives two lines:

  • The poll itself. The upload is 2.9 to 4.3 KB on Torizon OS 7, and 1.5 to 2.4 KB on TorizonCore 6.
  • About 60 s later, the close from the gateway. On Torizon OS 7 it is always +104 out +208 in: two connections. On TorizonCore 6 it is +52 out +104 in: one connection. The +72 out +144 in lines are the same single close over IPv6 (40-byte IP header instead of 20).
Torizon OS 7.7.0, RAC 395ff18
13:54:35 start: Torizon OS 7.7.0+build.40 (scarthgap), window 3600s, step 10s
13:54:35 remote-access: active, poll_timeout = { secs = 300, nanos = 0 }
13:54:35 aktualizr-torizon stopped for the window (it also talks to dgw.torizon.io)
13:54:35 counting 2a05:d014:662:ef01:5387:3765:f611:56b3 (route: )
13:54:35 counting 35.158.150.193 (route: dev ethernet0, addresses removed)
13:54:35 counting 63.181.150.207 (route: dev ethernet0, addresses removed)
13:55:26 dgw +104 out +208 in
13:59:26 dgw +4281 out +6512 in
14:00:27 dgw +104 out +208 in
14:04:28 dgw +4281 out +6511 in
14:05:28 dgw +104 out +208 in
14:09:29 dgw +2916 out +6565 in
14:10:29 dgw +104 out +208 in
14:14:30 dgw +3717 out +4857 in
14:15:30 dgw +104 out +208 in
14:19:31 dgw +3717 out +4857 in
14:20:31 dgw +104 out +208 in
14:24:32 dgw +4283 out +6563 in
14:24:42 half: 60932 bytes after 1807s, 12 bursts
14:25:32 dgw +104 out +208 in
14:29:33 dgw +4281 out +6566 in
14:30:33 dgw +104 out +208 in
14:34:34 dgw +3717 out +4858 in
14:35:34 dgw +104 out +208 in
14:39:35 dgw +4279 out +6511 in
14:40:25 dgw +104 out +208 in
14:44:26 dgw +4284 out +6564 in
14:45:26 dgw +104 out +208 in
14:49:27 dgw +3716 out +4857 in
14:50:28 dgw +104 out +208 in
14:54:28 dgw +3717 out +4805 in
14:54:38 window: 3603s, 24 bursts (about 12 polls expected at 300 s)
14:54:38 dgw total: 48437 out + 72522 in = 120959 bytes
14:54:38 per burst: 5039 bytes
14:54:38 first half: 87.4 MB/month, second half: 86.6 MB/month
14:54:38 whole window: 87.0 MB/month
14:54:39 one uncached lookup of dgw.torizon.io: 142 out + 202 in = 344 bytes (TTL 30 s: RAC pays it at every poll)
14:54:39 remote-access journal in the window: 0 line(s)
14:54:39 aktualizr-torizon started again
14:54:39 rules removed, done
TorizonCore 6.8.9, RAC 15845bc
13:54:52 start: TorizonCore 6.8.9+build.46 (kirkstone), window 3600s, step 10s
13:54:52 remote-access: active, poll_timeout = { secs = 300, nanos = 0 }
13:54:56 aktualizr-torizon stopped for the window (it also talks to dgw.torizon.io)
13:54:56 counting 2a05:d014:662:ef01:5387:3765:f611:56b3 (route: dev mlan0, addresses removed)
13:54:56 counting 35.158.150.193 (route: dev mlan0, addresses removed)
13:54:56 counting 63.181.150.207 (route: dev mlan0, addresses removed)
13:56:57 dgw +1716 out +1750 in
13:57:57 dgw +72 out +144 in
14:01:58 dgw +2222 out +3336 in
14:02:58 dgw +52 out +104 in
14:06:59 dgw +1540 out +3285 in
14:07:59 dgw +52 out +104 in
14:12:00 dgw +1656 out +1630 in
14:13:00 dgw +52 out +104 in
14:17:01 dgw +1538 out +3337 in
14:18:02 dgw +52 out +104 in
14:21:52 dgw +1716 out +1750 in
14:22:53 dgw +72 out +144 in
14:25:03 half: 26532 bytes after 1807s, 12 bursts
14:26:54 dgw +2301 out +3405 in
14:27:54 dgw +72 out +144 in
14:31:55 dgw +1538 out +3337 in
14:32:55 dgw +52 out +104 in
14:36:56 dgw +1656 out +1630 in
14:37:56 dgw +52 out +104 in
14:41:57 dgw +2301 out +3477 in
14:42:57 dgw +72 out +144 in
14:46:58 dgw +2301 out +3478 in
14:47:58 dgw +72 out +144 in
14:51:59 dgw +2386 out +3548 in
14:52:59 dgw +72 out +144 in
14:55:00 window: 3604s, 24 bursts (about 12 polls expected at 300 s)
14:55:00 dgw total: 23615 out + 35451 in = 59066 bytes
14:55:00 per burst: 2461 bytes
14:55:00 first half: 38.1 MB/month, second half: 46.9 MB/month
14:55:00 whole window: 42.5 MB/month
14:55:01 one uncached lookup of dgw.torizon.io: 142 out + 202 in = 344 bytes (TTL 30 s: RAC pays it at every poll)
14:55:01 remote-access journal in the window: 0 line(s)
14:55:01 aktualizr-torizon started again
14:55:01 rules removed, done

The D-Bus test sender is only about questions 4 and 5, not about the data use. If your developers want it, I can send it too.

About Torizon OS 7

Yes, sooner rather than later. Torizon OS 7.7.0 already runs on our test devices and on a first production device. We are now in the final tests before we deploy it on our fleet. We plan to move the rest of the fleet in waves over the coming weeks. Our devices fall back to 4G when they have no Wi-Fi, on a 100 MB/month plan, so the idle data use of RAC matters a lot for us.

Best regards,
M

Alright I’ve submitted the investigation and your additional data to our team. I’ll have them take a look at it with the goal of creating a solution to either reduce and/or better manage the data consumption on the remote-access process.

I assume if we can get the data usage back to around Torizon OS 6 levels, then that suffice for your needs, correct?

Best Regards,
Jeremias

Hello Jeremias,

Thank you very much for passing this to the team!

Yes, back to the TorizonCore 6 level would solve the main problem. Either fix works for us: one connection per poll, or an option to turn off the remote-commands loop.

One detail: at our 300 s poll_timeout, the TorizonCore 6 level is still about 45 MB/month with DNS. That is almost half of our 100 MB plan. So we will keep a longer poll_timeout on cellular, and send PollRasNow ourselves when we need a session. For this, the most useful answer for us is question 4: can we rely on the PollRasNow signal, its path and its bus name staying stable in later RAC versions?

Thanks again!

Best regards,
M

can we rely on the PollRasNow signal, its path and its bus name staying stable in later RAC versions?

This is a bit difficult to give a guarantee on. It’s unlikely we’ll have any reason to change this in the foreseeable future. But also, it’s not like we have an internal mandate to never change this.

The best answer I can give now is. It’s very unlikely this will change given the current circumstances, but the chance is not 0%.

Somewhat related the following changes were just added to rac, though they’re not in any official release yet: Provide a dbus api to trigger immediate RAS poll by simao · Pull Request #27 · torizon/rac · GitHub

Best Regards,
Jeremias