In the first article, we showed what LinkSense is — a lightweight, open-source synthetic monitoring tool. Here, we go one level deeper, into architectural decisions. Because in this project, almost every choice — from the communication model, through the programming language, to the database format — results from a specific problem, not from a trend.
Table of Contents
- The agent–server model: why the agent connects to the server, not the other way around
- Security: zero open ports on the agent side
- Mass reconfiguration from the server level
- Local-first: data primarily stored locally
- Aggregation: why every 60 seconds and batch writes
- Data model: what exactly goes into the database
- Why Rust
- Why SQLite
- Configuration as code: TOML
- Deployment: binaries, CI and the Linux ping trap
- Logs: why they do not go to standard output
- A deliberate “no”: containers
- Summary
The agent–server model: why the agent connects to the server, not the other way around
The starting point was one assumption: we want to have many agents in different locations. Everything else follows from that. Since there are supposed to be many agents and they are distributed around the world, the logical consequence is a central server to which they send data.
However, the key decision concerns the direction of the connection. In LinkSense, it is the agent that establishes a connection to the server, meaning it makes an outgoing connection, and not the server that “knocks” on the agents, meaning it initiates an incoming connection. Choosing this direction has two justifications — security and ease of management — and these defined the entire architecture.
The agent performs assigned tests, stores results locally, and every 60 seconds creates aggregated metrics and pushes them to the central server. If it cannot connect at a given moment, during the next attempt it additionally checks whether its configuration is up to date. The server receives the metrics and also stores them in its own local database.
Security: zero open ports on the agent side
This is the strongest argument for such a model. The agent can run on a machine with a firewall configured according to a classic, restrictive policy: “block incoming, allow outgoing”, meaning deny incoming, allow outgoing. Since it is the agent that initiates the connection outward, it does not need any open listening port.
Why is this important? Every open port is a potential entry point for an attacker. In security jargon, this means increasing the attack surface — the sum of all places through which someone could try to get into the system. Monitoring agents have been compromised in the past, and a compromised agent with an open port is a serious threat, because it is usually located deep inside the infrastructure. The absence of a listening port simply closes that path.
The second aspect is purely operational. Only one machine needs open ports — the server, because it collects the metrics. Securing one well-protected machine is incomparably simpler than maintaining open ports on dozens or hundreds of distributed hosts.
Mass reconfiguration from the server level
Since the agent queries the server regularly anyway, this can be used for remote reconfiguration. Agent configuration is changed centrally, on the server side — also in bulk mode, meaning bulk reconfigure, for many agents at once.
An example from the webinar: imagine a ship with sixteen or more agents on board. You want to add one new task to all of them. Instead of logging in to each machine separately, you add one entry in the configuration file on the server side.
The mechanism works as a “pull” initiated by the agent and is resistant to errors:
- You edit the configuration on the server, for example by uncommenting tasks.
- The server notes that it has a newer version of the configuration for a given agent than the one the agent currently has.
- During the next update, the agent reports: “my configuration is outdated, send me a newer one”.
- The server sends the new version, the agent validates it, replaces its local one and immediately starts using it.
During the live demonstration, this change propagated within a few seconds — the uncommented tasks immediately appeared in the agent’s raw metrics.
Local-first: data primarily stored locally
The architecture is “local-first”, meaning “locally first”: the agent saves results on its own side before sending anything. There are three consequences:
- no data loss, even when the server temporarily does not respond;
- continuity of monitoring during network issues — the agent does not stop working just because it has no connectivity with the central server; once connectivity is restored, it sends the collected metrics;
- the ability to query data locally, directly on the agent, which makes debugging much easier.
This also leads to standalone mode, meaning fully independent operation: the agent does not need a central server at all. If you use LinkSense as a small private project, it is enough to inspect the local database with any tool and, if needed, build alerting on top of that data yourself.
Aggregation: why every 60 seconds and batch writes
The agent does not send every single measurement separately. First, it aggregates them, and the server receives an aggregated batch in a one-minute interval. Importantly, this is a deliberate compromise, not an accident.
Consider a high-frequency scenario: you ping something every second or every 5 seconds, and from a large number of agents — say, hundreds. If every measurement were sent immediately, you would flood the network and the server with traffic. Aggregation every minute turned out to be the best compromise between accuracy and the amount of data transmitted and stored. The number “60 seconds” itself is not magical — it is a value chosen practically.
Aggregation does not lose important information. The aggregated batch includes, among other things: average times for all measured stages, maximum total time, the number of samples, meaning sample count, and the success percentage, meaning success rate. Thanks to the maximum value, you can see not only that “on average it was OK”, but also the worst case in a given window.
Additionally, writes to the database are performed in batches, meaning batch, and not one record at a time. This is another performance-oriented measure: fewer write operations mean lower overhead.
Data model: what exactly goes into the database
For people who will later consume this data, the most interesting part is how rich a single measurement is. An HTTP task does not return only “works/does not work”. It breaks the response down into specific components:
- TCP times, meaning connection establishment;
- TLS times, meaning encryption setup;
- time-to-first-byte, meaning the time until the first byte of the response;
- total download time;
- information on whether the connection was encrypted, meaning TLS, whether the certificate is valid and how many days remain until it expires;
- HTTP status codes, meaning information on whether there was success or an error.
Each task also includes the IP address of the device with which the connection was actually established. Thanks to this, you can see, for example, how the resolved IP address changes over time.
There is also an optional, very practical meta field: target_id. It allows different tasks related to the same target to be marked with the same identifier, for example ping, HTTP GET and TCP pointing to the same application, so that they can later be easily grouped and filtered. This field exists solely to help organize your own monitoring.
Why Rust
The performance visible in the numbers — an agent using around 31 MB of memory and 0–1% CPU with several dozen tasks — did not come out of nowhere. The team put a lot of work into making the program run as efficiently as possible, and this determined the choice of Rust as the language.
Two features are key here. First, asynchronous execution, meaning the async/await model: instead of blocking on every single test and waiting for its result, the agent runs many tasks concurrently, without wasting processor time on idle waiting. Second, performance and low memory usage — Rust does not have a garbage collector and allows precise control over resources.
This was also confirmed in tests: the software was run in a long-term run lasting more than a month, under significant load, as part of stress tests on the team’s own infrastructure. The goal was to catch potential memory leaks or performance degradation. The tests ended without a single problem.
Why SQLite
Data goes into SQLite — both on the agent side and on the server side. SQLite is a lightweight, file-based database: it does not require a separate database process or server, and everything is contained in a single file.
The choice of SQLite — and precisely these very simple table structures — was intentional. The goal was to make sure that:
- integration with anything would be trivial: to move data “somewhere else”, it is enough to read it from SQLite and copy it to the target system;
- local-only mode would be possible: you can inspect the database with any SQLite browser or another visualization or alerting tool, without setting up any additional infrastructure.
The simplicity of the schema is not cosmetic here. It is exactly what makes LinkSense easy to “attach” to an existing stack.
Configuration as code: TOML
The entire configuration consists of text files in TOML format — a readable configuration format based on key–value pairs. It is divided into layers:
- agent configuration — including the address of the central server and the key, meaning app key, needed for authentication; in local-only mode, these fields are unnecessary;
- task configuration — the list of what the agent should measure; adding a new task means copying an entry and changing a few values, and each task type has its own short README;
- server configuration — even simpler: listening address, its own key, the location for storing agent configurations and the size of the throughput test.
Since everything is text, the whole setup naturally supports versioning, for example in git. You have a history of changes and the ability to review them — meaning configuration as code.
Deployment: binaries, CI and the Linux ping trap
You can build LinkSense from source, but you do not have to. The project has GitHub Actions, meaning a CI mechanism — automatic building and releasing from the repository level — which prepares ready-made releases for many architectures. You download the binary appropriate for your platform and run it.
There is one trap worth remembering: ping permissions on Linux. If you want to run many pings in parallel, you need to add the user or process to the ping group. The reason is technical: ping on Linux requires privileged access, meaning a so-called raw socket — a low-level socket for sending ICMP packets — and the way Linux restricts this has changed over the years.
The creators had a choice: either use the system ping command as a simple wrapper — easier, but with the loss of asynchronous execution and performance — or implement ping themselves, asynchronously and efficiently, which requires the mentioned permission. They consciously chose the second option, in the name of performance.
Logs: why they do not go to standard output
A small but carefully considered detail: by design, the agent does not print logs to standard output, meaning stdout. This is the result of the fact that the tool is supposed to run “unattended”, including in remote locations. Logs are available in log files — and that is exactly where you should look for them if you want to trace something.
A deliberate “no”: containers
To the question about a ready-made containerized version, the answer is: “yes and no”. The idea is being considered, but for now the team does not see much value in it — and this is also an architectural decision.
The logic is consistent with the rest of the project: LinkSense is a single simple binary, and deployment comes down to the “copy the binary and run the service” model. Containerizing it yourself is trivial — all you need is your own Dockerfile or docker compose that copies the binary and starts the service.
What is more, in a container you begin to lose the argument about minimal resource consumption, which is one of the tool’s main advantages. This can be limited by using a lightweight image — Alpine Linux and a build for musl, meaning an alternative lightweight C standard library — although the authors have not tested this path themselves, so they treat it as “it should work”.
Summary
LinkSense architecture is a series of consistent, deliberate decisions: the agent connects to the server because it is safer; data is stored locally because we do not want to lose it; aggregation is minute-based because it is a reasonable compromise; the database is SQLite because the tool is supposed to integrate easily; the language is Rust because it is supposed to be fast and efficient.
Nothing here is accidental — and that is exactly why the whole solution remains so lightweight.
Want to learn more about LinkSense directly from its creators? Watch the dedicated webinar with Maciej Wilamowski and Marcin Kaźmierczak, where they talk about the architectural decisions behind the project, show how LinkSense works in practice and explain in which scenarios it can provide real support in monitoring distributed environments.

