This is a debugging story with a moral about documentation. It starts with
a bug report that looks impossible and ends with strings on a
closed-source binary, which is not where debugging stories are supposed to
end in 2026.
The symptom
BabySOARus runs Python inside Splunk searches. A user sorts their events in Python -- the most ordinary thing imaginable:
| makeresults count=40 | streamstats count as n
| exec inline="events.sort(key=lambda e: e['n'], reverse=True)"
And splunkd replies:
Invalid '_chunked_idx' from external search process: out of range or out of order
Three facts made it maddening. It only happened above roughly 35 rows.
It only happened on reordering -- filtering was fine, expanding
was fine, pass-through was fine. And the error names a field,
_chunked_idx, that appears nowhere in Splunk's developer
documentation for the protocol it belongs to.
The red herring
The first theory was _time: Splunk cares about event order,
event order usually means time, and stripping underscore-prefixed fields
before sorting made the error go away. Theory confirmed, warning written
into the docs -- "do not reorder events" -- everyone moves on.
Except the theory was wrong, and the fix worked by accident: stripping
underscore fields also strips _chunked_idx, which was the
actual trigger. This is the failure mode where a wrong explanation
survives because its remedy happens to overlap with the right one. It
survived here for a while too, as a documented limitation that should
never have been one.
The documentation dead end
Searching Splunk's documentation for _chunked_idx finds
nothing. The old docs domain 301s to a new help site; the page the
redirect lands on says the documentation has moved and points at a
developer-portal overview that does not mention the field. The protocol's
public specification describes chunks, metadata and payloads, and says
nothing about hidden columns in your data.
So: the authoritative source had to be the binary.
$ strings $SPLUNK_HOME/bin/splunkd | grep -B2 -A2 _chunked_idx
...
ChunkedExternProcessor
_chunked_idx
...
There it is, sitting directly beside ChunkedExternProcessor,
the class that runs external commands. And in
etc/system/default/messages.conf, the error's own identity:
[CHUNKED:MERGE_ERROR__S]
Merge. That word is the entire answer. splunkd tags every
row it sends an external command with a hidden _chunked_idx
column, and uses the values that come back to merge the command's output
into the rest of its pipeline. The merge requires them in range and
non-decreasing across the whole chunk. We were passing that column
through to guest Python as if it were user data -- it looks exactly like
user data -- so sorting events sorted splunkd's own
bookkeeping with it.
Why 35 rows
The daemon feeds guest code in micro-batches of 32. Below one batch, a reorder produces one locally renumbered run, which still satisfies "non-decreasing". At two batches, the second batch restarts its numbering below where the first ended, and the merge check fails. Hence the threshold: not magic, just 32 plus a few rows of protocol framing.
We proved the model before trusting it: renumbering
_chunked_idx per micro-batch inside guest Python still failed
at 40 rows; renumbering from a counter that persists across batches
passed. Which also proves the fix cannot live in guest code -- a snippet
only ever sees its own micro-batch, and the invariant spans the whole
chunk. Only the layer that owns the chunk-to-batch split has the full
picture.
The fix
In the protocol shim, which owns that split: take _chunked_idx
out of every record before guest code sees it, and put the values back by
position afterwards. Filtering yields a prefix of the original values;
expansion repeats the last one; both are in range and non-decreasing,
which is everything splunkd asks. Verified across every batch semantic at
100 rows -- reorder, filter, 3× expansion, drop-everything,
pass-through -- including a data-association check that every
_time still travelled with its own row after a full reversal.
Removing splunkd's private bookkeeping from user-visible data was the real
fix; the protocol error was just how it announced itself. The "do not
reorder events" warning came out of the documentation, replaced by the
true constraint: a sort inside one command invocation sorts within a
batch, and a whole-result-set sort belongs in SPL's own
| sort, where it always did.
Morals
- A fix that works for the wrong reason is a time bomb.
The
_timetheory produced a working remedy and a false limitation, and the limitation would have shipped forever. - The primary source is sometimes the binary.
stringsand the product's ownmessages.confanswered what the documentation could not. The word "merge" in an error identifier did more work than every search of the developer portal. - Hidden fields in user data are a footgun with a long fuse. If a protocol smuggles bookkeeping through the user's rows, every downstream consumer inherits an invariant it cannot see. If you design protocols: carry your bookkeeping out of band, or one of your users will spend a day like this one.