Low-level discovery: custom LLD rules and prototypes
-
Phase 1: Zabbix server and agents
-
Phase 2: Items, triggers, templates and dashboards
-
Phase 3: Alerts, proxies, SNMP and automation
Zabbix low-level discovery (LLD) lets Zabbix find things on a host by itself (file systems, network interfaces, disks, services, websites) and create items, triggers and graphs for each one from a set of prototypes. Without it you’d add “free space on /var” by hand on every server, and forget it on the one where someone mounted /data last week.
In this lesson you look inside the built-in file system discovery you already have from the Linux template, filter and override it, and set per-mount disk thresholds with user macros that have context. Then you write your own discovery rule with a small script that returns JSON for a list of services, build item and trigger prototypes on it, and break discovery in the three ways that silently create nothing.
Where this lesson fits
Lesson 15 of 24 in Zabbix Monitoring · Phase 2: Items, triggers, templates and dashboards · Before this: Lesson 14, custom checks · Next: Lesson 16, email alerts
| Time | You need | By the end you can |
|---|---|---|
| About 85 minutes | zbx01 and web01 from Lessons 1 to 14, Linux by Zabbix agent active linked to web01 | Read LLD JSON, build discovery rules with filters, overrides and prototypes, use macros with context for thresholds, and fix rules that discover nothing |
What it is, in one line
Zabbix low-level discovery is a special item (a discovery rule) that returns a JSON list of things on a host; for each entry Zabbix fills LLD macros such as {#FSNAME} and creates real items, triggers and graphs from prototypes.
In Simple Words
The school attendance register
At the start of each term, the class teacher doesn’t write a new register by hand. The office sends her a list of the students admitted to her section, and she copies the same columns (name, roll number, attendance for each day) for every name on the list.
When a new child joins mid-term, the next list from the office includes him and a new row appears. When a child leaves, his row is kept for a while for the records and then removed.
The office list is the discovery JSON, the columns are the prototypes, and keeping a row for a while before removing it is what Zabbix calls lost resources.
Engineer's View
The pieces of a discovery rule
A discovery rule is an item that returns JSON: an array of objects, one per discovered thing. Each object has fields, and the rule maps them to LLD macros, either because the field names already look like {#FSNAME} or through LLD macro paths such as {#FSNAME} = $.fsname. The server then creates one real object per entry from each prototype, replacing the macros.
| Part | What it does | Example |
|---|---|---|
| Discovery rule | Collects the JSON list on an interval (often 1h) | vfs.fs.get[short] on web01 |
| LLD macros | Names for fields in each entry | {#FSNAME}, {#FSTYPE}, {#SERVICE} |
| Filters | Keep or drop entries by regex on a macro | {#FSTYPE} matches ^(ext4|xfs)$ |
| Item, trigger, graph prototypes | Templates for objects per entry | vfs.fs.dependent.size[{#FSNAME},pused] |
| Overrides | Change prototypes for some entries only | Do not create the trigger for /boot |
| Lost resources | What to do when an entry disappears | Disable immediately, delete after 7 days |
Thresholds per entry use user macros with context: {$VFS.FS.PUSED.MAX.WARN:"{#FSNAME}"} looks for a macro for that exact file system first, for example {$VFS.FS.PUSED.MAX.WARN:"/var"}, and falls back to plain {$VFS.FS.PUSED.MAX.WARN} (80 in the Linux template). In 8.0 the defaults for lost resources are Delete after 7d and Disable immediately, and nested discovery (discovery prototypes, added in 7.4) is there for advanced cases; plain discovery works the same as in 7.0 LTS.
How it works, step by step
1The rule returns JSON
On web01 the Linux template’s rule “Mounted filesystem discovery” runs vfs.fs.get[short] every hour, which returns a JSON array with fsname, fstype and usage for every mount. The rule’s LLD macros tab maps {#FSNAME} to $.fsname and {#FSTYPE} to $.fstype.
2Filters decide which entries count
The template’s filter keeps file system types like ext4 and xfs and drops paths like /dev, /sys, /run and /proc, all through macros such as {$VFS.FS.FSTYPE.MATCHES}. Only entries that pass every condition go on to prototypes. The diagram below follows one entry through the rule.
3Prototypes become real objects
For each remaining entry the server creates items such as “FS [/]: Space: Used, in %” with key vfs.fs.dependent.size[/,pused], plus triggers like “FS [/]: Space is low”. Created objects are read-only on the host; you change them by editing the prototype, and the next discovery run updates them all.
4Lost resources are cleaned up later
If a file system disappears from the JSON (unmounted, or filtered out), its items aren’t deleted at once. With the 8.0 defaults they are disabled immediately and deleted after 7 days, so a short unmount doesn’t wipe history. The second diagram shows the lifecycle of one discovered entry.
5Built-in or custom discovery
| You want to discover | Use | LLD macros |
|---|---|---|
| Mounted file systems | vfs.fs.get or vfs.fs.discovery | {#FSNAME}, {#FSTYPE} |
| Network interfaces | net.if.get (8.0) or net.if.discovery | {#IFNAME} |
| systemd services | agent 2 systemd.unit.discovery[service] | {#UNIT.NAME}, {#UNIT.ACTIVESTATE} |
| Anything else (sites, queues, app instances) | A UserParameter or script that prints JSON | Your own, like {#SERVICE} |
| Items inside a JSON reply you already collect | Dependent discovery rule on that item | Mapped with LLD macro paths |
The JSON format is the same everywhere: an array of objects. Old-style output wrapped it as {"data":[...]}, which Zabbix still accepts, but a plain array is the current form.
Hands-on lab: discover file systems and watched services on web01
Before you run any command, double-check it
Read every command twice before you press Enter. Check the target: the server name, the user, the disk or device name, the path and the sizes. These commands are for learning and were tested in our lab, not on your system. At work, run the read-only commands first, follow your change process, make sure a backup exists, and ask a second person to check anything that changes or deletes data. You run them at your own risk.
Agent work happens on web01 and frontend work at https://zbx01.lab. web01 must be linked to Linux by Zabbix agent active (Lesson 4). You’ll add a second disk-like mount with a loop file so discovery has something new to find.
1Look at the raw discovery JSON
Run this on web01:
sudo zabbix_agent2 -t 'vfs.fs.discovery' | head -c 400; echoYou should see the start of the JSON (your mount list will differ):
vfs.fs.discovery [s|[{"{#FSNAME}":"/proc","{#FSTYPE}":"proc","{#FSOPTIONS}":"rw,relatime"},{"{#FSNAME}":"/sys","{#FSTYPE}":"sysfs","{#FSOPTIONS}":"rw,relatime"},{"{#FSNAME}":"/dev","{#FSTYPE}":"devtmpfs","{#FSOPTIONS}":"rw,relatime,size=4097460k,nr_inodes=1024365,mode=755"},That’s real agent 2 output: one object per mount, with field names already in LLD macro form. The template uses the newer vfs.fs.get, whose objects have plain field names (fsname, fstype, bytes, inodes) and need LLD macro paths, but the idea is the same.
✅ Checkpoint: You can see the JSON array of mounts and recognise {#FSNAME} and {#FSTYPE}.
2Add a new mount and let discovery find it
Run this on web01:
sudo fallocate -l 200M /srv/data.img
sudo mkfs.ext4 -q /srv/data.img
sudo mkdir -p /data && sudo mount -o loop /srv/data.img /data
df -h /dataYou should see:
Filesystem Size Used Avail Use% Mounted on
/dev/loop0 172M 24K 158M 1% /dataDiscovery runs every hour, so speed it up: in Data collection > Hosts > web01 > Discovery, tick “Mounted filesystem discovery” and press Execute now. Within a minute or two, Latest data for web01 shows new items for /data.
Latest data (filter “FS [/data]”) then shows the new items:
![Zabbix low-level discovery: Latest data with the discovered FS [/data] items on web01](https://saatvedha.com/wp-content/uploads/2026/11/zabbix-low-level-discovery-screen-1.png)
✅ Checkpoint: Items for FS [/data] appear on web01 without you creating any of them.
3Set a per-mount threshold with a macro context
Say /data may run fuller than the rest. On web01 open the Macros tab and add two host macros: {$VFS.FS.PUSED.MAX.WARN:"/data"} with value 95 and {$VFS.FS.PUSED.MAX.CRIT:"/data"} with value 98.
You need both, because the template’s “Space is critically low” trigger fires above 90% by default and the “Space is low” warning depends on it. Then fill /data past 80% and watch which trigger fires:
Run this on web01:
sudo fallocate -l 140M /data/fill.bin
df -h /dataYou should see:
Filesystem Size Used Avail Use% Mounted on
/dev/loop0 172M 141M 18M 90% /dataZabbix calculates about 89% here (df rounds up).
The general rule would fire “Space is low” (warn at 80), but /data’s own macro says 95, so no problem opens for /data while / still uses 80. Write a little more (sudo fallocate -l 10M /data/more.bin) to reach about 95.4%, and after five minutes (the trigger uses min(...,5m)) the warning appears for /data only. Context macros are how one template serves hundreds of mounts with different limits.
✅ Checkpoint: At about 90% no “Space is low” problem exists for /data; above 95% it does.
4Write a custom discovery script
Now your own rule: discover the services listed in a file and check each one is running. The script prints LLD JSON.
Run this on web01:
printf 'nginx\ncron\nssh\nzabbix-agent2\n' | sudo tee /etc/zabbix/watch-services.txt
sudo tee /etc/zabbix/scripts/svc_discovery.sh >/dev/null <<'EOF'
#!/bin/bash
# LLD: one entry per service name in watch-services.txt
LIST=/etc/zabbix/watch-services.txt
first=1
printf '['
while read -r svc; do
[ -z "$svc" ] && continue
case "$svc" in \#*) continue ;; esac
[ $first -eq 0 ] && printf ','
printf '{"{#SERVICE}":"%s"}' "$svc"
first=0
done < "$LIST"
printf ']\n'
EOF
sudo chmod 755 /etc/zabbix/scripts/svc_discovery.sh
sudo -u zabbix /etc/zabbix/scripts/svc_discovery.shYou should see:
nginx
cron
ssh
zabbix-agent2
[{"{#SERVICE}":"nginx"},{"{#SERVICE}":"cron"},{"{#SERVICE}":"ssh"},{"{#SERVICE}":"zabbix-agent2"}]The first four lines are tee echoing the list; the last line is the discovery JSON, which I checked with Zabbix 8.0’s own JSON parser. Now register two keys, one for discovery and one flexible key to check a service:
Run this on web01:
sudo tee /etc/zabbix/zabbix_agent2.d/userparameter_lld.conf >/dev/null <<'EOF'
UserParameter=svc.discovery,/etc/zabbix/scripts/svc_discovery.sh
UserParameter=svc.active[*],systemctl is-active --quiet "$1" && echo 1 || echo 0
EOF
sudo zabbix_agent2 -R userparameter_reload
sudo zabbix_agent2 -t 'svc.active[nginx]'You should see:
User parameters reloaded
svc.active[nginx] [s|1]✅ Checkpoint: svc_discovery.sh prints valid JSON as the zabbix user, and svc.active[nginx] returns 1.

5Create the discovery rule and prototypes
On web01 (or better, in the SaatVedha Nginx web template) open Discovery > Create discovery rule: Name Watched services, Type Zabbix agent (active), Key svc.discovery, Update interval 10m, Delete lost resources After 1d, Disable lost resources Immediately. Then add prototypes:
| Prototype | Name | Key or expression | Other |
|---|---|---|---|
| Item | Service {#SERVICE}: running | svc.active[{#SERVICE}] | Zabbix agent (active), Numeric (unsigned), 1m, tag service = {#SERVICE} |
| Trigger | Service {#SERVICE} is not running on {HOST.NAME} | max(/web01/svc.active[{#SERVICE}],3m)=0 | High |
Press Execute now on the rule. The trigger uses max(...,3m)=0 so a service must be down for three minutes in a row, which ignores a quick restart.
Latest data, filtered by the tag service, shows:

✅ Checkpoint: Four “Service …: running” items exist on web01, all created by discovery.
6Add and remove entries, watch lost resources
Run this on web01:
echo 'chrony' | sudo tee -a /etc/zabbix/watch-services.txt
sudo sed -i '/^cron$/d' /etc/zabbix/watch-services.txt
/etc/zabbix/scripts/svc_discovery.shYou should see:
chrony
[{"{#SERVICE}":"nginx"},{"{#SERVICE}":"ssh"},{"{#SERVICE}":"zabbix-agent2"},{"{#SERVICE}":"chrony"}]Execute the rule again. A new item for chrony appears, and the cron item shows a yellow info icon in the item list saying it’s no longer discovered and will be deleted in about a day; with “Disable immediately” it also stops collecting at once. If chrony isn’t installed, its item reports 0 and its trigger fires after three minutes, which is correct.
✅ Checkpoint: chrony has an item, and the cron item is disabled with a “no longer discovered” note.
7Use an override to skip a trigger for one service
Suppose ssh is managed by another team and you only want its item, not its trigger. In the rule’s Overrides tab add an override named No trigger for ssh: Filter {#SERVICE} matches ^ssh$, Operation: Object Trigger prototype, Condition name contains is not running, action Create enabled: No (or Discover: No). Execute the rule.
The ssh item stays, but its trigger is removed or created disabled. Overrides keep one discovery rule for everything instead of copying rules per team.

✅ Checkpoint: After execution, web01 has no enabled “Service ssh is not running” trigger, but still has the ssh item.
Break It, Then Fix It
Discovery rarely fails loudly; usually it creates nothing and you wonder why. Here are the three main causes.
1Invalid JSON: the rule goes Not supported
Break the script so it prints a trailing comma, a common mistake when building JSON in shell:
Run this on web01:
sudo sed -i "s/^printf ']/printf ',]/" /etc/zabbix/scripts/svc_discovery.sh
/etc/zabbix/scripts/svc_discovery.shYou’ll get this output, and then on the rule after Execute now:
[{"{#SERVICE}":"nginx"},{"{#SERVICE}":"ssh"},{"{#SERVICE}":"zabbix-agent2"},{"{#SERVICE}":"chrony"},]
invalid JSON object value starting character at: ']'That second line is the real message from Zabbix 8.0’s JSON parser for this input; the rule goes Not supported with it. Other inputs give other real messages: single quotes give invalid object name at: ..., plain text gives invalid object format, expected opening character '{' or '[' at: ..., and a missing bracket gives invalid array format, expected closing character ']'. Existing discovered items keep their data, but nothing new is created.
Fix:
Put the script back with sudo sed -i "s/^printf ',]/printf ']/" /etc/zabbix/scripts/svc_discovery.sh and test its output with python3 -m json.tool before trusting it. For anything bigger than a list of names, build the JSON with Python’s json module or jq rather than printf.
✅ Checkpoint: The script output passes python3 -m json.tool and the rule is supported again.
2Macro name mismatch: rule OK, nothing created
Edit the item prototype key to svc.active[{#SERVICES}] (an extra S) and execute the rule.
You’ll get this (info icon on the rule):
Cannot create item "svc.active[{#SERVICES}]": item with the same key already exists.{#SERVICES} doesn’t exist in the JSON, so it stays as literal text: the first entry creates one useless item with that literal key, and every other entry produces the same key and fails with this message, once per entry. The real items for each service are now lost resources and start to be disabled. The same thing happens when the JSON says {#SERVICE_NAME} but prototypes use {#SERVICE}, or when LLD macro paths point at a field that isn’t there.
Fix:
Copy macro names from the real JSON, not from memory. Open the rule, press Test (or run the script), and compare names character by character; LLD macros are case sensitive.
✅ Checkpoint: With {#SERVICE} back in the key, items are created for every service again.
3A filter that excludes everything
On the rule’s Filters tab add {#SERVICE} matches ^Nginx$ (capital N) and execute.
You’ll get this:
Rule state: Normal, no error
Items: all "Service ...: running" items show the yellow "no longer discovered" iconThe rule works, the JSON is fine, and the filter dropped every entry because regex matching is case sensitive and the service is “nginx”. Lost resource rules then start disabling and later deleting items, which is the dangerous part: a careless filter change on a template can disable monitoring across hundreds of hosts.
Fix:
Use ^nginx$, or (?i)^nginx$ if case varies. Test filters on one host before changing a template, and keep “Delete lost resources” at a few days or more so a mistake is recoverable.
✅ Checkpoint: After fixing the filter, the nginx item is discovered again and the others behave as you intended.
This happened at work
The mount point nobody added
This one is from a data centre operator in Navi Mumbai, told by the engineer who was on shift. A customer’s storage team mounted a new 4 TB volume at /backup on twelve database servers on a Tuesday. On Saturday night it filled up, the nightly dumps failed, and the customer found out on Monday.
The servers were in Zabbix with an old hand-made template that had fixed items for /, /var and /home. Nobody had added /backup, because nobody knew it existed. The on-call engineer first checked whether alerts were muted, and then whether the disk triggers had the wrong thresholds.
The fix was to move those servers to the standard Linux template, whose file system discovery found /backup within an hour, plus a context macro {$VFS.FS.PUSED.MAX.WARN:"/backup"} = 90 because backup volumes run fuller on purpose. They also set “Delete lost resources” to 30 days, so temporary unmounts during maintenance don’t erase history.
Common mistakes and troubleshooting
| Mistake | What happens | What to do instead |
|---|---|---|
| Building JSON with printf and string joins | A stray comma or quote makes the rule Not supported | Use python3 json or jq, and check with python3 -m json.tool |
| Typing LLD macro names from memory | Nothing is created, or items clash | Copy the names from the real JSON; they are case sensitive |
| Delete lost resources: Immediately | A short unmount or a filter mistake wipes history | Disable immediately, delete after several days |
| Copying the whole rule to change one threshold | Two rules discovering the same things, duplicate keys | Use a macro with context or an override |
| Discovery interval of 1 minute | Extra load for lists that rarely change | 1h is normal; use Execute now when testing |
| Editing discovered items on the host | Changes are refused or lost on the next run | Edit the prototype instead |
| Filters tested on a template first | A wrong regex disables items on every linked host | Test on one host, then move to the template |
Interview questions
Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.
Fresher level
1. What is low-level discovery in Zabbix, in one minute?
Why they ask this: They want the concept and the moving parts.
How you might answer: A discovery rule is an item that returns a JSON list of things on a host, like file systems or services. For each entry Zabbix fills LLD macros such as {#FSNAME} and creates items, triggers and graphs from prototypes.
When an entry disappears, lost resource settings disable and later delete the objects, so the monitoring follows the host without manual work.
2. How would you set a different disk threshold for /backup than for the rest of the file systems?
Why they ask this: Checks knowledge of macros with context.
How you might answer: I’d use a user macro with context, like {$VFS.FS.PUSED.MAX.WARN:”/backup”} = 90, on the host or template. The trigger prototype uses {$VFS.FS.PUSED.MAX.WARN:”{#FSNAME}”}, so Zabbix uses the context value for /backup and the default macro for others.
I wouldn’t copy the discovery rule or edit discovered triggers by hand.
2–5 years experience
3. Your custom discovery rule shows no error but creates no items. Walk me through your checks.
Why they ask this: Troubleshooting order for silent LLD failures.
How you might answer: First I run the script or key myself as the zabbix user and check the JSON with python3 -m json.tool. Then I compare the macro names in the JSON with the ones in the prototypes, because a typo means nothing resolves.
Then I check the Filters tab, since a filter that matches nothing gives a healthy rule with zero entries, and finally the info icons on the rule and items, which show messages like duplicate keys.
4. What do the lost resources settings do and how would you set them?
Why they ask this: Operational judgement about history and cleanup.
How you might answer: Disable lost resources stops collecting for items whose entry disappeared, and Delete lost resources removes them later with their history. In 8.0 the defaults are disable immediately and delete after 7 days.
For volumes and services that may come back after maintenance I keep delete at a week or more, so a short outage or a filter mistake doesn’t erase history.
Senior level
5. How would you monitor 400 microservice instances that come and go every hour?
Why they ask this: Senior design: discovery at scale and churn.
How you might answer: I’d avoid host-level items per instance and use discovery from a central source, for example an HTTP agent master item that reads the service registry or an exporter endpoint, with a dependent discovery rule and dependent item prototypes from the same reply. That keeps it to one request per cycle.
I’d set short lost resource times since instances are short-lived, aggregate at the service level for alerting, and consider Prometheus for very high churn, bringing only service-level health into Zabbix.
6. A filter change on a template disabled monitoring for half your fleet. How do you prevent that?
Why they ask this: Change control around LLD.
How you might answer: Discovery filters and overrides live in templates, so I version templates in Git and review the YAML diff before import, test on a single host first, and keep Delete lost resources long enough to recover. A trigger on the internal item for the number of unsupported items per host also helps catch it early.
After an incident I’d fix the filter, execute the rule, and items come back with their history if they were only disabled.
Quick quiz
Q1. What does a discovery rule return?
- A single number
- A JSON array of entries
- A list of triggers
- A YAML template
Show answer
B — Each object in the JSON array is one discovered entry with fields mapped to LLD macros.
Q2. Which macro form gives /data its own threshold?
- {$VFS.FS.PUSED.MAX.WARN}
- {#VFS.FS.PUSED.MAX.WARN:/data}
- {$VFS.FS.PUSED.MAX.WARN:”/data”}
- {VFS.FS.PUSED.MAX.WARN[/data]}
Show answer
C — User macros with context use {$MACRO:”context”} and fall back to the plain macro.
Q3. A script prints [{“{#SERVICE}”:”nginx”},]. What happens?
- Zabbix ignores the comma
- The rule goes Not supported with a JSON parse error
- Only nginx is discovered
- A service named “,” is created
Show answer
B — A trailing comma is invalid JSON, and the rule reports the parser error.
Q4. What are the 8.0 defaults for lost resources?
- Delete immediately
- Never delete
- Disable immediately, delete after 7 days
- Disable after 30 days
Show answer
C — The schema defaults are lifetime 7d and disable immediately.
Q5. Prototypes use {#SERVICES} but the JSON has {#SERVICE}. What do you see?
- Everything works
- Items cannot be created because the macro is not resolved
- The rule deletes the host
- Zabbix renames the macro
Show answer
B — LLD macros must match character for character; an unresolved macro produces errors or duplicate keys.
Quick recap
- A discovery rule returns a JSON array; LLD macros name its fields and prototypes turn each entry into real items, triggers and graphs.
- Filters choose which entries count, overrides change prototypes for some entries, and lost resource settings handle entries that disappear.
- Macros with context, like {$VFS.FS.PUSED.MAX.WARN:”/data”}, give per-entry thresholds without copying rules.
- Custom discovery is a script or UserParameter that prints valid JSON; test it as the zabbix user and with a JSON checker.
- When nothing is created, check JSON validity, macro names and filters, in that order.
Next lesson
Lesson 16: Zabbix alerts by email: media types, actions and message macros → You send problem and recovery emails through Mailpit on zbx01, with actions, escalations and message templates.
Further reading: Zabbix low-level discovery
Want to go deeper into Zabbix low-level discovery? The official Low-level discovery, Custom LLD rules and User macros with context pages cover every option. For the agent side of custom keys, see Zabbix UserParameter, and for how templates carry discovery rules, Zabbix templates.
You can also see the full plan for this course on the Zabbix Monitoring course page.
