elastic/elasticsearch · error · IllegalArgumentException
circular reference detected: {}
Error message
circular reference detected: {} What it means
Thrown by PatternBank's constructor (and extendWith) when a named grok pattern transitively references itself. The constructor runs forbidCircularReferences, which depth-first walks the directed graph of %{NAME} references; when the walk returns to the start node with a non-trivial stack, the path is reported as a cycle. The message lists the offending reference chain (start->...->start) so the conflicting pattern definitions are visible.
Source
Thrown at libs/grok/src/main/java/org/elasticsearch/grok/PatternBank.java:109
*/
Deque<String[]> stack = new ArrayDeque<>();
stack.push(new String[] { traversalStartNode });
// This is used so that we know that we're unwinding the stack and know not to get the current node's neighbors again.
boolean unwinding = false;
while (stack.isEmpty() == false) {
String[] currentLevel = stack.peek();
int firstNonNullIndex = findFirstNonNull(currentLevel);
String node = currentLevel[firstNonNullIndex];
boolean endOfThisPath = false;
if (unwinding) {
// We have completed all of this node's neighbors and have popped back to the node
endOfThisPath = true;
} else if (traversalStartNode.equals(node) && stack.size() > 1) {
Deque<String> reversedPath = new ArrayDeque<>();
for (String[] level : stack) {
reversedPath.push(level[findFirstNonNull(level)]);
}
throw new IllegalArgumentException("circular reference detected: " + String.join("->", reversedPath));
} else if (visitedFromThisStartNode.contains(node)) {
/*
* We are only looking for a cycle starting and ending at traversalStartNode right now. But this node has been
* visited more than once in the path rooted at traversalStartNode. This could be because it is a cycle, or could be
* because two nodes in the path both point to it. We add it to nodesVisitedMoreThanOnceInAPath so that we make sure
* to check the path rooted at this node later.
*/
nodesVisitedMoreThanOnceInAPath.add(node);
endOfThisPath = true;
} else {
visitedFromThisStartNode.add(node);
String[] neighbors = getPatternNamesForPattern(bank, node);
if (neighbors.length == 0) {
endOfThisPath = true;
} else {
stack.push(neighbors);
}
}View on GitHub (pinned to db6a809a66)
Solutions
- Read the -> chain in the message: it names the exact patterns forming the loop (e.g. A->B->A). Break the cycle by removing or rewriting one of those entries.
- Audit every %{NAME} token in each named pattern in the chain and confirm each NAME resolves to a definition that does not lead back to the original.
- Build the bank incrementally with extendWith to bisect which added pattern introduced the cycle.
- If the cycle is intentional (aliasing), expand the alias inline so the graph stays acyclic, since PatternBank forbids cycles outright.
Example fix
// before
Map<String,String> patterns = new LinkedHashMap<>();
patterns.put("IP", "%{HOSTNAME}");
patterns.put("HOSTNAME", "%{IP}"); // cycle
PatternBank bank = new PatternBank(patterns); // throws
// after
patterns.put("IP", "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
patterns.put("HOSTNAME", "\\b(?:[a-zA-Z0-9]+\\.)+[a-zA-Z]+\\b");
PatternBank bank = new PatternBank(patterns); // ok Defensive patterns
Strategy: validation
Validate before calling
// Validate a pattern map for cycles before constructing the bank.
static void assertAcyclic(java.util.Map<String,String> patterns) {
java.util.Set<String> visiting = new java.util.HashSet<>();
java.util.Set<String> done = new java.util.HashSet<>();
for (String name : patterns.keySet()) dfs(name, patterns, visiting, done);
}
private static void dfs(String name, java.util.Map<String,String> patterns,
java.util.Set<String> visiting, java.util.Set<String> done) {
if (done.contains(name) || !patterns.containsKey(name)) return;
if (!visiting.add(name)) throw new IllegalArgumentException("cycle at " + name);
String body = patterns.get(name);
java.util.regex.Matcher m = java.util.regex.Pattern.compile("%\\{(\\w+)").matcher(body);
while (m.find()) dfs(m.group(1), patterns, visiting, done);
visiting.remove(name);
done.add(name);
} Type guard
// Narrow to a bank whose construction succeeded (no cycle possible post-construction)
static boolean isAcyclicMap(java.util.Map<String,String> patterns) {
try { new org.elasticsearch.grok.PatternBank(patterns); return true; }
catch (IllegalArgumentException e) { return false; }
} Try / catch
try {
PatternBank bank = new PatternBank(userPatterns);
} catch (IllegalArgumentException e) {
if (e.getMessage().startsWith("circular reference detected")) {
// e.getMessage() contains the offending A->B->A chain
reportConfigError("grok patterns contain a cycle: " + e.getMessage());
} else throw e;
} Prevention
- Treat the pattern catalog as code: lint it in CI by attempting new PatternBank(map) before deploy.
- Use extendWith to layer patterns so the cycle source is isolated to the most recently added layer.
- Keep pattern names in a single registry and grep for %{NAME} before deleting a definition.
When it happens
Trigger: Constructing `new PatternBank(map)` or calling `patternBank.extendWith(extra)` where two or more entries form a cycle, e.g. {"A":"%{B}", "B":"%{A}"} or a self-reference {"A":"%{A}"}. The check runs once at construction time, so the exception surfaces at the new PatternBank(...) call, not at match time.
Common situations: Loading a user-supplied grok pattern catalog (Logstash-style patterns.conf, ingest grok processor patterns) where one definition was renamed but a stale reference remained; copy-pasting a pattern that delegates to another which was later edited to delegate back; merging two pattern sets that each redefine a name pointing at the other.
Related errors
- pattern [{}] has an invalid syntax
- pattern [{}] is referencing a non-existent pattern [{}]
- circular reference in pattern back [{}]
- Can not convert grok patterns to regular expression
- unsupported ECS compatibility mode [{}]
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/40d35b0361a827ae.
Report an issue: GitHub.