diff --git a/lucene/core/src/java/org/apache/lucene/util/automaton/CompiledAutomaton.java b/lucene/core/src/java/org/apache/lucene/util/automaton/CompiledAutomaton.java
index dbe24de6ce9..fb6508a67d0 100644
--- a/lucene/core/src/java/org/apache/lucene/util/automaton/CompiledAutomaton.java
+++ b/lucene/core/src/java/org/apache/lucene/util/automaton/CompiledAutomaton.java
@@ -240,11 +240,7 @@ public class CompiledAutomaton implements Accountable {
     if (this.finite) {
       commonSuffixRef = null;
     } else {
-      // NOTE: this is a very costly operation!  We should test if it's really warranted in
-      // practice... we could do a fast match
-      // by looking for a sink state (which means it has no common suffix).  Or maybe we shouldn't
-      // do it when simplify is false?:
-      BytesRef suffix = Operations.getCommonSuffixBytesRef(binary, maxDeterminizedStates);
+      BytesRef suffix = Operations.getCommonSuffixBytesRef(binary);
       if (suffix.length == 0) {
         commonSuffixRef = null;
       } else {
diff --git a/lucene/core/src/java/org/apache/lucene/util/automaton/Operations.java b/lucene/core/src/java/org/apache/lucene/util/automaton/Operations.java
index ad080aff6b8..e0427660570 100644
--- a/lucene/core/src/java/org/apache/lucene/util/automaton/Operations.java
+++ b/lucene/core/src/java/org/apache/lucene/util/automaton/Operations.java
@@ -39,9 +39,12 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+
+import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.lucene.util.FixedBitSet;
 import org.apache.lucene.util.IntsRef;
 import org.apache.lucene.util.IntsRefBuilder;
 import org.apache.lucene.util.RamUsageEstimator;
@@ -1050,38 +1053,68 @@ public final class Operations {
 
   /**
    * Returns the longest string that is a prefix of all accepted strings and visits each state at
-   * most once. The automaton must be deterministic.
+   * most once. The automaton must not have dead states.
    *
+   * @throws IllegalArgumentException if the automaton has dead states reachable from the initial state.
    * @return common prefix, which can be an empty (length 0) String (never null)
    */
   public static String getCommonPrefix(Automaton a) {
-    if (a.isDeterministic() == false) {
-      throw new IllegalArgumentException("input automaton must be deterministic");
-    }
-    StringBuilder b = new StringBuilder();
-    HashSet<Integer> visited = new HashSet<>();
-    int s = 0;
-    boolean done;
-    Transition t = new Transition();
-    do {
-      done = true;
-      visited.add(s);
-      if (a.isAccept(s) == false && a.getNumTransitions(s) == 1) {
-        a.getTransition(s, 0, t);
-        if (t.min == t.max && !visited.contains(t.dest)) {
-          b.appendCodePoint(t.min);
-          s = t.dest;
-          done = false;
+    if (hasDeadStatesFromInitial(a)) {
+      throw new IllegalArgumentException("input automaton has dead states");
+    }
+    if (isEmpty(a)) {
+      return "";
+    }
+    StringBuilder builder = new StringBuilder();
+    Transition scratch = new Transition();
+    FixedBitSet visited = new FixedBitSet(a.getNumStates());
+    FixedBitSet current = new FixedBitSet(a.getNumStates());
+    FixedBitSet next = new FixedBitSet(a.getNumStates());
+    current.set(0); // start with initial state
+    algorithm:
+    while (true) {
+      // if we've already seen ALL these states before, we are done.
+      if (FixedBitSet.intersectionCount(visited, current) == current.cardinality()) {
+        break algorithm;
+      }
+      int label = -1;
+      // do a pass, stepping all current paths forward once
+      for (int state = current.nextSetBit(0); 
+          state != DocIdSetIterator.NO_MORE_DOCS; 
+          state = state + 1 >= current.length() ? DocIdSetIterator.NO_MORE_DOCS : current.nextSetBit(state + 1)) {
+        visited.set(state);
+        // if it is an accept state, we are done
+        if (a.isAccept(state)) {
+          break algorithm;
+        }
+        for (int transition = 0; transition < a.getNumTransitions(state); transition++) {
+          a.getTransition(state, transition, scratch);
+          if (label == -1) {
+            label = scratch.min;
+          }
+          // either a range of labels, or label that doesn't match all the other paths this round
+          if (scratch.min != scratch.max || scratch.min != label) {
+            break algorithm;
+          }
+          // mark target state for next iteration
+          next.set(scratch.dest);
         }
       }
-    } while (!done);
-
-    return b.toString();
+      // none of the states went anywhere (e.g. accept states), we are done
+      if (label == -1) {
+        break algorithm;
+      }
+      // add the label to the prefix
+      builder.appendCodePoint(label);
+      // swap "current" with "next", clear "next"
+      FixedBitSet tmp = current;
+      current = next;
+      next = tmp;
+      next.clear(0, next.length());
+    }
+    return builder.toString();
   }
 
-  // TODO: this currently requites a determinized machine,
-  // but it need not -- we can speed it up by walking the
-  // NFA instead.  it'd still be fail fast.
   /**
    * Returns the longest BytesRef that is a prefix of all accepted strings and visits each state at
    * most once. The automaton must be deterministic.
@@ -1089,23 +1122,15 @@ public final class Operations {
    * @return common prefix, which can be an empty (length 0) BytesRef (never null)
    */
   public static BytesRef getCommonPrefixBytesRef(Automaton a) {
+    String prefix = getCommonPrefix(a);
     BytesRefBuilder builder = new BytesRefBuilder();
-    HashSet<Integer> visited = new HashSet<>();
-    int s = 0;
-    boolean done;
-    Transition t = new Transition();
-    do {
-      done = true;
-      visited.add(s);
-      if (a.isAccept(s) == false && a.getNumTransitions(s) == 1) {
-        a.getTransition(s, 0, t);
-        if (t.min == t.max && !visited.contains(t.dest)) {
-          builder.append((byte) t.min);
-          s = t.dest;
-          done = false;
-        }
+    for (int i = 0; i < prefix.length(); i++) {
+      char ch = prefix.charAt(i);
+      if (ch > 255) {
+        throw new IllegalStateException("automaton is not binary");
       }
-    } while (!done);
+      builder.append((byte) ch);
+    }
 
     return builder.get();
   }
@@ -1144,15 +1169,13 @@ public final class Operations {
 
   /**
    * Returns the longest BytesRef that is a suffix of all accepted strings. Worst case complexity:
-   * exponential in number of states (this calls determinize).
+   * nobody knows.
    *
-   * @param maxDeterminizedStates maximum number of states determinizing the automaton can result
-   *     in. Set higher to allow more complex queries and lower to prevent memory exhaustion.
    * @return common suffix, which can be an empty (length 0) BytesRef (never null)
    */
-  public static BytesRef getCommonSuffixBytesRef(Automaton a, int maxDeterminizedStates) {
+  public static BytesRef getCommonSuffixBytesRef(Automaton a) {
     // reverse the language of the automaton, then reverse its common prefix.
-    Automaton r = Operations.determinize(reverse(a), maxDeterminizedStates);
+    Automaton r = reverse(a);
     BytesRef ref = getCommonPrefixBytesRef(r);
     reverseBytes(ref);
     return ref;
diff --git a/lucene/core/src/test/org/apache/lucene/util/automaton/TestAutomaton.java b/lucene/core/src/test/org/apache/lucene/util/automaton/TestAutomaton.java
index cc827f43a05..8ba8b3001f1 100644
--- a/lucene/core/src/test/org/apache/lucene/util/automaton/TestAutomaton.java
+++ b/lucene/core/src/test/org/apache/lucene/util/automaton/TestAutomaton.java
@@ -250,7 +250,7 @@ public class TestAutomaton extends LuceneTestCase {
     a.addTransition(init, fini, 'm');
     a.addTransition(fini, fini, 'm');
     a.finishState();
-    assertEquals(0, Operations.getCommonSuffixBytesRef(a, DEFAULT_MAX_DETERMINIZED_STATES).length);
+    assertEquals(0, Operations.getCommonSuffixBytesRef(a).length);
   }
 
   public void testReverseRandom1() throws Exception {
