{"id":6168,"date":"2026-09-05T00:43:54","date_gmt":"2026-09-04T15:43:54","guid":{"rendered":"https:\/\/eternalsphere.net\/echoes\/?p=6168"},"modified":"2026-09-12T17:17:49","modified_gmt":"2026-09-12T08:17:49","slug":"2538rcirkvhj9vi","status":"publish","type":"post","link":"https:\/\/blog.eternalsphere.net\/index.php\/2026\/09\/05\/2538rcirkvhj9vi\/","title":{"rendered":"Monitoring Tor Relay Bandwidth and Building a Persistent Contribution Counter"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Tor operators often want answers to two simple questions:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>How much bandwidth is the relay using right now?\n\nHow much traffic has it contributed in total?<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tor exposes enough information through its control protocol to answer both.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Real-time counters are already available<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Tor control interface provides values equivalent to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>traffic\/read\ntraffic\/written\nuptime<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These counters describe bytes read and written by the Tor daemon since the current Tor process started.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A monitoring script can sample them twice:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>counter at T1\n      \u2193\nwait 1 second\n      \u2193\ncounter at T2\n      \u2193\ndifference \u00f7 elapsed time\n      \u2193\ncurrent bandwidth<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>RX : 2.4 Mbit\/s\nTX : 2.2 Mbit\/s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The values can also be displayed in bytes per second:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>0.300 MB\/s\n0.275 MB\/s<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Why RX and TX should both be shown<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A Middle Relay usually receives encrypted traffic and forwards it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Therefore a single relayed stream generally generates both:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>RX\n+\nTX<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If 1 GiB arrives and approximately 1 GiB leaves, the server has processed roughly 2 GiB of network traffic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For this reason:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>RX + TX<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">is useful for estimating physical network usage, but it should not be interpreted as unique user payload.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Protocol overhead, directory activity, control traffic, and retransmission may also be included.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A lightweight command-line dashboard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A useful local tool can provide:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>===== TOR TRAFFIC =====\n\nCurrent bandwidth\n  RX : ...\n  TX : ...\n  SUM: ...\n\nCurrent Tor process traffic\n  RX : ...\n  TX : ...\n  SUM: ...\n\nPermanent accumulated Tor traffic\n  RX : ...\n  TX : ...\n  SUM: ...\n\nTor uptime : ...\nCounter    : persistent<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two modes are particularly convenient:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tor-traffic<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">for a one-second snapshot, and:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tor-traffic -w<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">for continuous monitoring.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use the alternate terminal screen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A continuous terminal monitor should not destroy existing shell output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Programs such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>top\nhtop\nless\nnyx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">use the terminal\u2019s alternate screen buffer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The correct sequence is conceptually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>normal shell screen\n        \u2193\nenter alternate screen\n        \u2193\ndisplay live dashboard\n        \u2193\nrefresh in place\n        \u2193\nCtrl+C\n        \u2193\nleave alternate screen\n        \u2193\noriginal shell screen restored<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">ANSI terminal control sequences can implement this behavior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is preferable to repeatedly running:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>clear<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">because clearing before every refresh creates visible flicker.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Real-time monitoring does not require disk I\/O<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A real-time bandwidth monitor can read Tor counters entirely through a local Unix control socket.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The path may resemble:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/run\/tor\/control<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The data flow is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Tor memory counters\n       \u2193\nUnix socket\n       \u2193\nmonitoring process\n       \u2193\nterminal<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This does not inherently write anything to disk.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CPU and memory overhead are extremely small.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Permanent counters require persistence<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Tor counters reset when the Tor daemon restarts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To maintain lifetime statistics, an independent collector can store:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>total_rx\ntotal_tx\nlast_rx\nlast_tx\nlast_uptime\nupdated_at<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">in a tiny state file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Example architecture:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Tor\n \u2193\ncollector samples every second\n \u2193\ndelta calculated in RAM\n \u2193\nperiodic state save\n \u2193\npersistent JSON file<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If Tor restarts and current counters return to small values, the collector recognizes the reset and adds the new values to the previously accumulated totals.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Avoid unnecessary disk writes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A na\u00efve collector might write its state every second.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is unnecessary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The state file is tiny, but the filesystem operation itself can still generate:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>metadata updates;<\/li>\n\n\n\n<li>journal writes;<\/li>\n\n\n\n<li>block flushes;<\/li>\n\n\n\n<li>SSD or HDD activity.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A better design samples frequently but persists infrequently.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For long-term relay statistics:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sample interval: 1 second\ndisk save interval: 1 hour<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">is completely reasonable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The collector continues calculating values in RAM, while storage is touched only periodically.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The tradeoff<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">With a one-hour save interval:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>normal shutdown\n\u2192 state saved cleanly\n\ncollector restart\n\u2192 state saved cleanly\n\nunexpected power failure\n\u2192 up to roughly one hour of unsaved totals may be lost<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For operational statistics, this is often an acceptable compromise.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A relay processing traffic continuously for years does not need second-perfect lifetime totals.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use atomic writes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Persistent state should not be overwritten directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A safer pattern is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>write temporary file\n      \u2193\nfsync\n      \u2193\nrename temporary file over state file<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">POSIX rename operations are atomic within the same filesystem.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This greatly reduces the chance of leaving a partially written JSON file after a crash.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Run the collector as an unprivileged service<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The persistent collector only needs access to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Tor Unix control socket\nits own state directory<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It does not need root.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A systemd service can therefore use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>User=&lt;TOR_SERVICE_ACCOUNT&gt;\nGroup=&lt;TOR_SERVICE_GROUP&gt;\n\nNoNewPrivileges=true\nPrivateTmp=true\nProtectHome=true\nProtectSystem=strict<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with a dedicated state directory created by systemd.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The result is a very small monitoring service with minimal privileges.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Keep monitoring private<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Detailed real-time relay traffic is operationally useful but should generally remain private.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A local SSH dashboard is appropriate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>administrator\n   \u2193 SSH\nserver\n   \u2193\ntor-traffic -w<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Publishing second-by-second ingress and egress graphs for an individual Tor relay provides unnecessary traffic-analysis information.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Public dashboards should favor coarse, aggregated historical statistics rather than precise live measurements.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A sustainable monitoring architecture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A clean long-term setup therefore looks like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Tor daemon\n   \u2502\n   \u251c\u2500\u2500 Nyx for interactive inspection\n   \u2502\n   \u251c\u2500\u2500 tor-traffic for local snapshots\n   \u2502\n   \u2514\u2500\u2500 persistent collector\n          \u2193\n       RAM updates every second\n          \u2193\n       disk save every hour<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This provides useful real-time visibility, permanent contribution totals, negligible resource usage, and very little additional disk activity.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a volunteer relay intended to run continuously for years, that balance is usually far more useful than deploying a large monitoring stack.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Tor operators often want answers to two simple questions: Tor exposes enough information through its control protocol to answer both. Real-time counters are already available The Tor control interface provides values equivalent to: These counters describe bytes read and written by the Tor daemon since the current Tor process started. &hellip;<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[120],"tags":[245],"class_list":["post-6168","post","type-post","status-publish","format-standard","hentry","category-1s3b6h7r2zay02x","tag-tor"],"_links":{"self":[{"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/posts\/6168","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/comments?post=6168"}],"version-history":[{"count":1,"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/posts\/6168\/revisions"}],"predecessor-version":[{"id":6169,"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/posts\/6168\/revisions\/6169"}],"wp:attachment":[{"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/media?parent=6168"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/categories?post=6168"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.eternalsphere.net\/index.php\/wp-json\/wp\/v2\/tags?post=6168"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}