Zabbix preprocessing: regex, JSONPath, JavaScript and dependent items
-
Phase 1: Zabbix server and agents
-
Phase 2: Items, triggers, templates and dashboards
-
Phase 3: Alerts, proxies, SNMP and automation
Zabbix preprocessing is the small factory between “value received” and “value stored”, where raw text is cut, cleaned, converted and checked before it lands in history. Without it you’d write a shell script for every odd format; with it, one HTTP request to Nginx can become seven tidy numbers, a growing counter becomes requests per second, and a JSON reply gives up only the field you care about.
In this lesson you add a master item to the SaatVedha Nginx web template that fetches the Nginx stub_status page once, then build dependent items that pull numbers out of it with regular expressions, JavaScript and JSONPath. You’ll test every step in the frontend before saving, and then break preprocessing three ways that look scary in the item list and are easy to fix once you can read the message.
Where this lesson fits
Lesson 11 of 24 in Zabbix Monitoring · Phase 2: Items, triggers, templates and dashboards · Before this: Lesson 10, templates · Next: Lesson 12, web monitoring
| Time | You need | By the end you can |
|---|---|---|
| About 80 minutes | zbx01 and web01 from Lessons 1 to 10, the template SaatVedha Nginx web linked to web01, the frontend at https://zbx01.lab | Design a master item with dependent items, choose the right preprocessing step for a job, and read preprocessing errors without guessing |
What it is, in one line
Zabbix preprocessing is an ordered list of steps (regex, JSONPath, JavaScript, multiplier, change per second, throttling, validation and more) that the server or proxy runs on each value before storing it.
In Simple Words
The sabzi mandi sorting table
Picture a vegetable wholesaler at a mandi in Pune. A truck dumps one big sack of mixed vegetables on the sorting table; one worker pulls out the onions, the next picks out tomatoes, another weighs the potatoes and writes the number down, and anything rotten goes straight into the bin.
The truck came only once, but five different crates leave the table, each clean and labelled.
The truck is the master item, the crates are the dependent items, and the workers at the table are the preprocessing steps.
Engineer's View
Where preprocessing runs and what it can do
Preprocessing runs in the preprocessing manager and workers of the Zabbix server, or of the proxy if the host is monitored by one, never on the agent. Steps run top to bottom, the output of one is the input of the next, and the final result must fit the item’s Type of information (Numeric unsigned, Numeric float, Character, Log, Text, Binary, or the new JSON type in 8.0). If a step fails, the item becomes Not supported with the step’s error, unless that step’s Custom on fail says Discard value, Set value to, or Set error to.
| Group in the step list | Steps | Typical use |
|---|---|---|
| Text | Regular expression, Replace, Trim, Right trim, Left trim | Pull a number out of a status page or log line |
| Structured data | JSONPath, XML XPath, CSV to JSON, XML to JSON | Take one field from an API reply |
| Arithmetic and Change | Custom multiplier, Simple change, Change per second | KB to bytes, counters to rates |
| Numeral systems | Boolean to decimal, Octal to decimal, Hexadecimal to decimal | Values from devices |
| Custom scripts | JavaScript | Anything the other steps can’t express |
| Validation | In range, Matches regular expression, Check for error in JSON, Check for not supported value | Reject bad values, turn errors into clear messages |
| Throttling | Discard unchanged, Discard unchanged with heartbeat | Store a value only when it changes, plus one every N minutes |
| Prometheus | Prometheus pattern, Prometheus to JSON | Read exporter /metrics pages (Lesson 23) |
A dependent item has no interval of its own. It gets a copy of its master item‘s value the moment the master receives one, and runs its own preprocessing on that copy. So one HTTP request feeds many items, and the target server is asked once instead of seven times.
In 7.0 LTS the step list is the same apart from the JSON value type, which is new in 8.0.
How it works, step by step
1The master item fetches raw text
Agent 2 on web01 runs web.page.get[localhost,basic_status,80], which returns the HTTP headers plus the stub_status body. This item’s history is set to Do not store because nobody needs the raw page, only the numbers in it.
2Each dependent item gets a copy
When the master value arrives at the server, the preprocessing manager queues it for every dependent item. Each dependent runs its own steps, for example a regular expression with output \1 to take the first number after “Active connections:”. The diagram below follows one value from the agent to seven stored numbers.
3Counters become rates
Nginx counts requests since it started, so the raw number only grows. Change per second stores (new - old) / (seconds between them); the first value after a restart or item creation has nothing to compare against and is dropped quietly, and so is a value smaller than the previous one (a counter reset). That’s why a new rate item shows nothing for one interval.
4Steps fail, and you decide what happens
Every step can fail: a regex finds no match, a JSONPath points at a missing field, a script throws. By default the item becomes Not supported with that error and the value is lost. Custom on fail lets you discard quietly, put in a fixed value such as 0, or replace the message with one your on-call team understands.
The second diagram shows the choices.
5Testing before you save
Every item form has a Test button on the Preprocessing tab. You paste a sample value (or, for item types the server polls itself, such as passive agent, HTTP agent or SNMP, press Get value), and Zabbix shows the result of each step one by one. Active agent items can’t be fetched from the frontend because the agent connects to the server, not the other way round, so for them you paste a sample you captured with zabbix_get or curl.
For the JavaScript step there’s also zabbix_js, a command-line tool shipped with the server package, which runs the same Duktape engine the server uses.
| If you need | Use | Why not the others |
|---|---|---|
| One number from text | Regular expression with output \1 | Fast, no script to maintain |
| One field from JSON | JSONPath | Clear, handles nested objects and arrays |
| Several fields or maths | JavaScript | Regex can’t add or divide |
| A rate from a counter | Change per second | Built in, handles resets |
| Fewer stored rows | Discard unchanged with heartbeat | Still proves the item is alive |
Hands-on lab: turn one Nginx stub_status request into seven items
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.
You’ll add everything to the template SaatVedha Nginx web from Lesson 10, so web01 picks it up automatically. If you skipped Lesson 10, do the same work on web01 directly; only the trigger expressions would name web01 instead of the template. Commands run on web01 or zbx01 as stated.
1Turn on stub_status on web01
Nginx’s stub_status module is built into the Ubuntu package. Add a small location to the default site that only answers local requests.
Run this on web01:
sudo tee /etc/nginx/conf.d/stub_status.conf >/dev/null <<'EOF'
server {
listen 127.0.0.1:80;
server_name localhost;
location = /basic_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
EOF
sudo nginx -t && sudo systemctl reload nginx
curl -s http://127.0.0.1/basic_statusYou should see this (your counter values will differ):
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Active connections: 1
server accepts handled requests
36 36 34
Reading: 0 Writing: 1 Waiting: 0 The four lines are what Zabbix will parse. “accepts handled requests” are three counters since Nginx started; Reading, Writing and Waiting are current connection states.
✅ Checkpoint: curl on web01 prints the four stub_status lines.
2Check the master key with zabbix_get
Run this on zbx01 (with the PSK copy from Lesson 7 in /tmp/web01.psk):
zabbix_get -s 192.168.56.81 -k 'web.page.get[localhost,basic_status,80]' --tls-connect psk --tls-psk-identity 'PSK web01' --tls-psk-file /tmp/web01.pskYou should see this (your date and counters will differ):
HTTP/1.1 200 OK
Connection: close
Content-Length: 100
Content-Type: text/plain
Date: Sun, 08 Nov 2026 06:21:31 GMT
Server: nginx/1.28.3 (Ubuntu)
Active connections: 1
server accepts handled requests
38 38 36
Reading: 0 Writing: 1 Waiting: 0 That’s the raw value the master item will get: headers, a blank line, then the body. Your regexes must work on this whole text, not only on the four body lines.
The PSK options are needed because web01 accepts only encrypted connections since Lesson 6. If zabbix_get can’t connect, passive checks are blocked; the master item below is active, so you can carry on.
✅ Checkpoint: zabbix_get returns HTTP headers plus the stub_status body.
3Create the master item
Open the template (Data collection > Templates > SaatVedha Nginx web > Items > Create item):
| Field | Value |
|---|---|
| Name | Nginx: stub_status page |
| Type | Zabbix agent (active) |
| Key | web.page.get[localhost,basic_status,{$NGINX.PORT}] |
| Type of information | Text |
| Update interval | 1m |
| History | Do not store |
| Tags | component = nginx, component = raw |
Press Add. Because history is off, this item will show no value in Latest data; that’s expected, it only feeds the dependents.
✅ Checkpoint: The template has the item “Nginx: stub_status page” with History set to Do not store.
4Add regex dependent items
Click the master item’s name in the item list and choose Create dependent item from the menu, or create items with Type Dependent item and Master item “Nginx: stub_status page”. Make these four:
| Name | Key | Preprocessing (Regular expression: pattern, output) | Extra steps |
|---|---|---|---|
| Nginx: active connections | nginx.conn.active | Active connections: ([0-9]+) → \1 | |
| Nginx: requests total | nginx.requests.total | server accepts handled requests\s+([0-9]+) ([0-9]+) ([0-9]+) → \3 | |
| Nginx: requests per second | nginx.requests.rate | same pattern → \3 | Change per second; Type of information Numeric (float) |
| Nginx: connections waiting | nginx.conn.waiting | Reading: ([0-9]+) Writing: ([0-9]+) Waiting: ([0-9]+) → \3 |
Open Nginx: requests total, go to its Preprocessing tab and press Test. Paste the whole zabbix_get output from step 2 (headers included) into the Value box and press Test.
The Test window lists step 1, Regular expression, with the result 36 next to it, and the final Result is 36: the third number on the counters line of the text you pasted.

The rate item would show no result in the same test, because Change per second needs a previous value; the test window has Previous value and Prev. time fields if you want to try it by hand. In real collection the second value a minute later gives the first rate.
✅ Checkpoint: Four dependent items exist, and Test shows a number after the regex step.
5Turn the whole page into JSON with JavaScript
A regex per field is fine for four numbers, but the dropped-connections figure needs maths (accepts minus handled). A single JavaScript step can produce all fields as JSON, and later items read them with JSONPath. First test the script on zbx01 with zabbix_js, which comes with the zabbix-server package:
Run this on zbx01:
cat > /tmp/stub.txt <<'EOF'
Active connections: 1
server accepts handled requests
38 38 36
Reading: 0 Writing: 1 Waiting: 0
EOF
cat > /tmp/stub2json.js <<'EOF'
var m = value.match(/Active connections: (\d+)\s+server accepts handled requests\s+(\d+) (\d+) (\d+)\s+Reading: (\d+) Writing: (\d+) Waiting: (\d+)/);
if (!m) {
throw 'stub_status format not recognised';
}
return JSON.stringify({
active: Number(m[1]), accepts: Number(m[2]), handled: Number(m[3]),
requests: Number(m[4]), reading: Number(m[5]), writing: Number(m[6]),
waiting: Number(m[7]), dropped: Number(m[2]) - Number(m[3])
});
EOF
zabbix_js -s /tmp/stub2json.js -i /tmp/stub.txtYou should see:
{"active":1,"accepts":38,"handled":38,"requests":36,"reading":0,"writing":1,"waiting":0,"dropped":0}That’s real output from the Zabbix JavaScript engine. Now create a dependent item Nginx: status JSON, key nginx.status.json, Type of information Text (or JSON in 8.0), History 1d, with one preprocessing step JavaScript and the script body pasted in (everything inside the file, without the cat lines).
✅ Checkpoint: zabbix_js prints one JSON line, and the item Nginx: status JSON exists with a JavaScript step.

6Chain JSONPath items off the JSON item
A dependent item can itself be a master. Create two dependent items whose master is Nginx: status JSON:
| Name | Key | Preprocessing | Type of information |
|---|---|---|---|
| Nginx: connections dropped | nginx.conn.dropped | JSONPath $.dropped | Numeric (unsigned) |
| Nginx: connections writing | nginx.conn.writing | JSONPath $.writing | Numeric (unsigned) |
Test the first one: paste the JSON line that zabbix_js printed into the Value box and press Test. The input of a dependent item’s test is its own master’s value, here the JSON from Nginx: status JSON.
Step 1, JSONPath $.dropped, shows the result 0, and the final Result is 0.
Add a trigger in the template so dropped connections get noticed: min(/SaatVedha Nginx web/nginx.conn.dropped,5m)>0, name Nginx is dropping connections on {HOST.NAME}, severity Average.
✅ Checkpoint: Nginx: connections dropped and Nginx: connections writing test cleanly with JSONPath.
7Save storage with Discard unchanged with heartbeat
Waiting connections on a quiet lab server stay at 0 for hours, and storing 0 every minute is wasted rows. Open Nginx: connections waiting and add a second step Discard unchanged with heartbeat with parameter 15m.
Now Zabbix stores a value only when it changes, plus one every 15 minutes so nodata() triggers still know the item is alive. Don’t add this to items behind a last() or avg(...,5m) trigger that needs a point every minute.
✅ Checkpoint: Nginx: connections waiting has two steps: Regular expression, then Discard unchanged with heartbeat 15m.
8Load the site and read Latest data
Send some traffic from zbx01 so the counters move:
Run this on zbx01:
for i in $(seq 1 300); do curl -s -o /dev/null http://192.168.56.81/; done; echo doneIn Monitoring > Latest data, host web01, tag component = nginx, each dependent item now has a value. Nginx: requests total has gone up by about 300, Nginx: requests per second shows a few requests per second for the minute the loop ran, Nginx: connections dropped stays at 0, and the master item has no value because it stores no history.
Seven items from one request a minute. The rate depends on how fast your loop ran and on where the minute boundaries fell.

✅ Checkpoint: Latest data shows values for the dependent items and the rate went up during the curl loop.
Break It, Then Fix It
Three failures that land in the item list as a red “Not supported” icon. None of them harm the server.
1“cannot perform regular expression … pattern does not match”
Edit Nginx: active connections and change the pattern to Active Connections: ([0-9]+) (capital C). Save, wait a minute for the next value, and hover over the red i next to the dependent item.
You’ll get this error:
cannot perform regular expression "Active Connections: ([0-9]+)" match for value of type "none": pattern does not matchThe regular expression step is case sensitive and Nginx writes “Active connections”. The type shows as “none” because Zabbix clears the value before building the message, which confuses people the first time; it has nothing to do with your data type. The same error appears at work after an upgrade changes a status page’s wording, or when a load balancer sends an HTML error page instead of the real one.
Fix:
Put the pattern back to Active connections: ([0-9]+), or make it case-insensitive with (?i)active connections: ([0-9]+). Use the Test button with a real sample from zabbix_get to check the pattern against what the host sends.
✅ Checkpoint: The item shows a number again and the red i is gone.
2JavaScript error in the step
In Nginx: status JSON, change the first line of the script to read var m = value.mtch( (a typo), press Test and paste the four stub_status lines from step 5 as the value.
You’ll get this error:
cannot execute script: TypeError: undefined not callable (property 'mtch' of 'Active connections: 1 \x0aserver ac...')
at [anon] (duktape.c:69012) internal
at [anon] (function:1) preventsyieldStrings have no mtch method, so the engine throws at runtime. A missing bracket fails even earlier, at compile time; you can see both kinds with zabbix_js:
Run this on zbx01:
printf 'return value.toUpperCase(;\n' > /tmp/syntax.js
zabbix_js -s /tmp/syntax.js -p x
printf 'return JSON.parse(value).data.status;\n' > /tmp/undef.js
zabbix_js -s /tmp/undef.js -p '{"ok":1}'You’ll get this:
zabbix_js [918]: error executing script:
cannot compile script: SyntaxError: empty expression not allowed (line 1)
zabbix_js [923]: error executing script:
cannot execute script: TypeError: cannot read property 'status' of undefined
at [anon] (duktape.c:60534) internal
at [anon] (function:1) preventsyieldBoth outputs are real. Compile errors give a line number; runtime errors such as reading a property of something missing are the ones that surprise you later, when an API changes its reply.
Fix:
Correct the typo and test again. In scripts that read API replies, check fields before using them and throw a clear message, as the stub2json script does with “stub_status format not recognised”, so the item error tells on-call what went wrong.
✅ Checkpoint: Nginx: status JSON tests cleanly and its two JSONPath children show values again.
3Dependent items stuck because the master is not supported
Edit the master item and change the key to web.page.get[localhost,basic_status_x,{$NGINX.PORT}]. Wait a minute, then look at the dependent items.
You’ll get this:
Nginx: stub_status page (no value; History is off)
Nginx: active connections cannot perform regular expression "Active connections: ([0-9]+)" match for value of type "none": pattern does not match
Nginx: requests total ... pattern does not match
Nginx: connections dropped last value from 1 minute ago, then nothingweb.page.get doesn’t fail on a 404; it returns the 404 page, so the master “succeeds” with the wrong text and every dependent fails with a confusing regex error. If the master fails outright (wrong port, agent down), it goes Not supported and the dependents stop getting values silently, so their graphs freeze at the last value. Either way, the problem is upstream of the item that turned red.
Fix:
Put the key back. To make this obvious next time, give the master its own first preprocessing step Matches regular expression server accepts handled requests with Custom on fail Set error to stub_status page not found, check the Nginx location. Then the master itself goes Not supported with a clear message, and a nodata(/SaatVedha Nginx web/nginx.requests.total,5m)=1 trigger catches the silent case.
✅ Checkpoint: All dependent items update again, and the master has a validation step with a custom error.
This happened at work
The payment success rate that stopped at 99.2%
This one comes from the on-call engineer at a fintech in Pune. Their payment gateway exposed a JSON health endpoint, and Zabbix had one HTTP agent master item with about twenty dependent items: success rate, latency per bank, queue depth and so on. One Thursday evening the business team asked why the success-rate graph had been a flat 99.2% since lunch, when the support desk was full of failed UPI complaints.
The engineer first checked the trigger, which was fine, then the dependent item, which showed no error at all. The value was old. The master item was Not supported with Response code "401" did not match any of the required status codes "200", because a security team had rotated the API token at noon and nobody updated the macro holding it.
Nothing alerted, because every trigger used last() on the dependents, and last() kept returning the lunch-time value. The team added a nodata(...,5m)=1 trigger on the most important dependent, a validation step on the master with a clear error message, and a check on the master’s own state through the internal item zabbix[host,,items_unsupported] for that host.
Common mistakes and troubleshooting
| Mistake | What happens | What to do instead |
|---|---|---|
| Polling the same URL once per field | Seven requests a minute to Nginx instead of one, and values taken at slightly different times | One master item, many dependent items |
| Storing the master item’s raw text | Large text rows fill the history tables | Set History to Do not store (or a short period) on the master |
| Using Change per second on a gauge | Nonsense rates for values that go up and down | Use it only on counters that only grow |
| Regex written against the body only | No match, because the master value includes HTTP headers | Test against a real sample captured with zabbix_get or curl |
| Discard unchanged without heartbeat | nodata() triggers fire on stable items | Use Discard unchanged with heartbeat and a sensible period |
| Triggers using last() on dependent items only | When the master breaks, triggers keep seeing the old value | Add nodata() triggers and a validation step on the master |
| Huge JavaScript in one item | Hard to read, slow, and errors are vague | Keep scripts short, throw clear messages, and test with zabbix_js |
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 a dependent item, and why would you use one?
Why they ask this: Basic understanding of master and dependent items.
How you might answer: A dependent item has no interval of its own; it gets a copy of its master item’s value every time the master collects one and runs its own preprocessing. I use it when one request returns many values, like the Nginx stub_status page or a JSON API.
One web.page.get master gave me seven Nginx items in the lab, so Nginx was asked once a minute instead of seven times.
2. Where does preprocessing run: on the agent or on the server?
Why they ask this: They check you know the architecture and where load lands.
How you might answer: On the Zabbix server, or on the proxy if the host is monitored by a proxy, in the preprocessing manager and workers. The agent only sends the raw value.
So heavy JavaScript on thousands of items costs server or proxy CPU, which matters when sizing a proxy for a remote site.
2–5 years experience
3. Your dependent items all show “pattern does not match” at once. What is your first move?
Why they ask this: They want you to look upstream instead of fixing each regex.
How you might answer: When all dependents fail together, I look at the master first. I run the master key by hand with zabbix_get, or curl the URL, and read the raw value: often it’s an error page, a redirect or a changed format, not a regex problem.
Then I fix the source, and add a validation step on the master with a custom error so the next time the master itself says what’s wrong.
4. How do you convert a growing counter into a rate in Zabbix, and what happens on a restart?
Why they ask this: Checks Change per second and counter reset handling.
How you might answer: I add the Change per second step, which stores the difference divided by the seconds between two values. The first value has nothing to compare with and is dropped.
If the counter goes down, for example after an Nginx restart, Zabbix drops that value too instead of storing a negative rate, so the graph has one missing point and continues.
Senior level
5. You have 20,000 items with JavaScript preprocessing and the server is struggling. How do you approach it?
Why they ask this: Performance thinking at scale.
How you might answer: First I’d measure: the internal items for preprocessing queue and worker busy percent, and Monitoring > Queue. Then I’d look for scripts that run on every value but could be a regex or JSONPath, which are much cheaper.
I’d move heavy hosts to proxies so preprocessing is spread out, use master and dependent items to cut duplicate parsing, add Discard unchanged with heartbeat where values rarely change, and raise StartPreprocessors only after the cheaper fixes.
6. How would you design monitoring for a JSON API that returns 50 metrics, some of which come and go?
Why they ask this: Design question touching dependent items and discovery.
How you might answer: One HTTP agent master item fetching the JSON with History off, then dependent items with JSONPath for the fixed metrics. For the changing ones I’d use a dependent low-level discovery rule on the same master, so items are created and removed as the keys appear and disappear.
I’d add a validation step on the master (Check for error in JSON or a required field), a nodata() trigger on a key metric, and Custom on fail rules so a missing optional field doesn’t make items unsupported.
Quick quiz
Q1. Where does Zabbix run item preprocessing?
- On the agent
- On the server or proxy
- In the browser
- In the database
Show answer
B — Preprocessing workers run on the server, or on the proxy for proxy-monitored hosts.
Q2. Which step turns a growing request counter into requests per second?
- Custom multiplier
- Simple change
- Change per second
- In range
Show answer
C — Change per second divides the difference by the time between values.
Q3. A dependent item’s update interval comes from…
- Its own Update interval field
- Its master item
- The template
- The agent config
Show answer
B — Dependent items get a value whenever the master item collects one.
Q4. What does Discard unchanged with heartbeat 15m do?
- Deletes history older than 15 minutes
- Stores a value only when it changes, plus at least one every 15 minutes
- Checks the agent every 15 minutes
- Discards all values for 15 minutes
Show answer
B — It saves storage while still proving the item is alive.
Q5. All dependent items fail with “pattern does not match” at the same time. Most likely cause?
- A typo in every regex
- The master item returns different text (error page or changed format)
- The database is full
- The agent version is old
Show answer
B — When every child fails together, check the master value first.
Quick recap
- Preprocessing steps run in order on the server or proxy, before the value is stored.
- One master item plus many dependent items means one request to the target and many clean numbers.
- Regex for text, JSONPath for JSON, JavaScript for maths or many fields, Change per second for counters.
- Use the Test button and zabbix_js before saving, and read errors from the master downwards.
- Validation steps with Custom on fail and nodata() triggers stop broken masters from hiding behind old values.
Next lesson
Lesson 12: Web scenarios and HTTP agent for websites and APIs → You check a website the way a user does, step by step, and pull values from a JSON API with the HTTP agent, including certificate expiry.
Further reading: Zabbix preprocessing
Want to go deeper into Zabbix preprocessing? The official Item value preprocessing, Dependent items and JavaScript preprocessing pages list every step and its parameters, and the Nginx stub_status module page explains each counter. If the Nginx items are new to you, see Zabbix templates first, and compare with how Prometheus does rates in PromQL examples.
You can also see the full plan for this course on the Zabbix Monitoring course page.
