diff --git a/.clj-kondo/hooks/clojure/core.clj b/.clj-kondo/hooks/clojure/core.clj
index c9cd4c404cfd0a5a3dfa515f4df24c5e1ef655f5..809c56cbaa307d72b269595e7daaafb0dd3af0b9 100644
--- a/.clj-kondo/hooks/clojure/core.clj
+++ b/.clj-kondo/hooks/clojure/core.clj
@@ -43,6 +43,9 @@
      methodical.core/remove-aux-method-with-unique-key!
      next.jdbc/execute!
 
+     ;; Definitely thread safe
+     metabase.test.util.dynamic-redefs/patch-vars!
+
      ;; TODO: most of these symbols shouldn't be here, we should go through them and
      ;; find the functions/macros that use them and make sure their names end with !
      ;; best way to do this is try remove each of these and rely on kondo output to find places where it's used
diff --git a/test/metabase/test.clj b/test/metabase/test.clj
index 8d367b420d7a1cfb56d3836862e90fdc61c6233c..b5cde224bff9faea74445484ca9963f953a0c064 100644
--- a/test/metabase/test.clj
+++ b/test/metabase/test.clj
@@ -34,6 +34,7 @@
    [metabase.test.redefs :as test.redefs]
    [metabase.test.util :as tu]
    [metabase.test.util.async :as tu.async]
+   [metabase.test.util.dynamic-redefs :as tu.dr]
    [metabase.test.util.i18n :as i18n.tu]
    [metabase.test.util.log :as tu.log]
    [metabase.test.util.misc :as tu.misc]
@@ -314,3 +315,11 @@
 (alter-meta! #'with-temp update :doc str "\n\n  Note: by default, this will execute its body inside a transaction, making
   it thread safe. If it is wrapped in a call to [[metabase.test/test-helpers-set-global-values!]], it will affect the
   global state of the application database.")
+
+;; Cursive does not understand p/import-macro, so we just proxy this manually
+(defmacro with-dynamic-redefs
+  "A thread-safe version of with-redefs. It only support functions, and adds a fair amount of overhead.
+   It works by replacing each original definition with a proxy the first time it is redefined.
+   This proxy uses a dynamic mapping to check whether the function is currently redefined."
+  [bindings & body]
+  `(tu.dr/with-dynamic-redefs ~bindings ~@body))
diff --git a/test/metabase/test/util/dynamic_redefs.clj b/test/metabase/test/util/dynamic_redefs.clj
new file mode 100644
index 0000000000000000000000000000000000000000..daefdc77fcc810a0f08ea6b80d9a06467182fa5e
--- /dev/null
+++ b/test/metabase/test/util/dynamic_redefs.clj
@@ -0,0 +1,53 @@
+(ns metabase.test.util.dynamic-redefs
+  (:require [medley.core :as m])
+  (:import (clojure.lang Var)))
+
+(set! *warn-on-reflection* true)
+
+(def ^:dynamic *local-redefs*
+  "A thread-local mapping from vars to their most recently bound definition."
+  {})
+
+(defn- get-local-definition
+  "Get the version of this function that is in scope. It is the unpatched version if there is no override."
+  [a-var]
+  (get *local-redefs* a-var
+       (get (meta a-var) ::original)))
+
+(defn- var->proxy
+  "Build a proxy function to intercept the given var. The proxy checks the current scope for what to call."
+  [a-var]
+  (fn [& args]
+    (let [current-f (get-local-definition a-var)]
+      (apply current-f args))))
+
+(defn patch-vars!
+  "Rebind the given vars with proxies that wrap the original functions."
+  [vars]
+  (let [unpatched-vars (remove #(::patched? (meta %)) vars)]
+    (doseq [^Var a-var unpatched-vars]
+      (locking a-var
+        (when-not (::patched? (meta a-var))
+          (let [old-val (.getRawRoot a-var)
+                patch-meta #(assoc % ::original old-val ::patched? true)]
+            (.bindRoot a-var (with-meta (var->proxy a-var)
+                                        (patch-meta (meta (get *local-redefs* a-var)))))
+            (alter-meta! a-var patch-meta)))))))
+
+(defn- sym->var [sym] `(var ~sym))
+
+(defn- bindings->var->definition
+  "Given a with-redefs style binding, return a mapping from each corresponding var to its given replacement."
+  [binding]
+  (m/map-keys sym->var (into {} (partition-all 2) binding)))
+
+(defmacro with-dynamic-redefs
+  "A thread-safe version of with-redefs. It only supports functions, and adds a fair amount of overhead.
+   It works by replacing each original definition with a proxy the first time it is redefined.
+   This proxy uses a dynamic mapping to check whether the function is currently redefined."
+  [bindings & body]
+  (let [var->definition (bindings->var->definition bindings)]
+    `(do
+       (patch-vars! ~(vec (keys var->definition)))
+       (binding [*local-redefs* (merge *local-redefs* ~var->definition)]
+         ~@body))))
diff --git a/test/metabase/test/util_test.clj b/test/metabase/test/util_test.clj
index 7a02516eb5d7e4b550ab34fc8eaba97da6207981..ca1b9f15e4c71dc539ba98d669a9dd8b2e0a533e 100644
--- a/test/metabase/test/util_test.clj
+++ b/test/metabase/test/util_test.clj
@@ -7,7 +7,10 @@
    [metabase.test :as mt]
    [metabase.test.data :as data]
    [metabase.util :as u]
-   [toucan2.core :as t2]))
+   [toucan2.core :as t2])
+  (:import (java.util.concurrent CountDownLatch TimeUnit)))
+
+(set! *warn-on-reflection* true)
 
 (deftest with-temp-vals-in-db-test
   (testing "let's make sure this acutally works right!"
@@ -42,3 +45,44 @@
       (test-util-test-setting))
     (is (= ["A" "B" "C"]
            (test-util-test-setting)))))
+
+(defn- clump [x y] (str x y))
+
+(deftest ^:parallel with-dynamic-redefs-test
+  (testing "Three threads can independently redefine a regular var"
+    (let [n-threads 3
+          latch (CountDownLatch. (inc n-threads))
+          take-latch  #(do
+                         (.countDown latch)
+                         (when-not (.await latch 100 TimeUnit/MILLISECONDS)
+                           (throw (ex-info "Timeout waiting on all threads to pull their latch" {:latch latch}))))]
+
+      (testing "The original definition"
+        (is (= "original" (clump "o" "riginal"))))
+
+      (future
+        (testing "A thread that minds its own business"
+          (is (= "123" (clump 12 3)))
+          (take-latch)
+          (is (= "321" (clump 3 21)))))
+
+      (future
+        (testing "A thread that redefines it in reverse"
+          (mt/with-dynamic-redefs [clump #(str %2 %1)]
+            (is (= "ok" (clump "k" "o")))
+            (take-latch)
+            (is (= "ko" (clump "o" "k"))))))
+
+      (future
+        (testing "A thread that redefines it twice"
+          (mt/with-dynamic-redefs [clump #(str %2 %2)]
+            (is (= "zz" (clump "a" "z")))
+            (mt/with-dynamic-redefs [clump (fn [x _] (str x x))]
+              (is (= "aa" (clump "a" "z")))
+              (take-latch)
+              (is (= "mm" (clump "m" "l"))))
+            (is (= "bb" (clump "a" "b"))))))
+
+      (take-latch)
+      (testing "The original definition survives"
+        (is (= "original" (clump "orig" "inal")))))))
diff --git a/test/metabase/util/malli/defn_test.clj b/test/metabase/util/malli/defn_test.clj
index 1e223d5ff1539094a13ceb13d117f429aa63523f..8881d721606dcc390ed8b634c619fc41ec65e4fc 100644
--- a/test/metabase/util/malli/defn_test.clj
+++ b/test/metabase/util/malli/defn_test.clj
@@ -4,6 +4,7 @@
    [clojure.test :refer :all]
    [malli.core :as mc]
    [malli.experimental :as mx]
+   [metabase.test :as mt]
    [metabase.util.malli :as mu]
    [metabase.util.malli.defn :as mu.defn]
    [metabase.util.malli.fn :as mu.fn]))
@@ -161,16 +162,16 @@
   (is (= 'Integer
          (-> #'add-ints meta :arglists first meta :tag))))
 
-(deftest defn-forms-are-not-emitted-for-skippable-ns-in-prod-test
+(deftest ^:parallel defn-forms-are-not-emitted-for-skippable-ns-in-prod-test
   (testing "omission in macroexpansion"
     (testing "returns a simple fn*"
-      (with-redefs [mu.fn/instrument-ns? (constantly false)]
+      (mt/with-dynamic-redefs [mu.fn/instrument-ns? (constantly false)]
         (let [expansion (macroexpand `(mu/defn ~'f :- :int [] "foo"))]
           (is (= '(def f
                     "Inputs: []\n  Return: :int" (clojure.core/fn [] "foo"))
                  expansion)))))
     (testing "returns an instrumented fn"
-      (with-redefs [mu.fn/instrument-ns? (constantly true)]
+      (mt/with-dynamic-redefs [mu.fn/instrument-ns? (constantly true)]
         (let [expansion (macroexpand `(mu/defn ~'f :- :int [] "foo"))]
           (is (= '(def f
                     "Inputs: []\n  Return: :int"
diff --git a/test/metabase/util/malli/fn_test.clj b/test/metabase/util/malli/fn_test.clj
index 1a470a43574ad15fb0b4df5482985f1c9551d8a9..2a65b6f5e25d5d4ab9d5390a27ab950d165eb74c 100644
--- a/test/metabase/util/malli/fn_test.clj
+++ b/test/metabase/util/malli/fn_test.clj
@@ -252,14 +252,14 @@
                     (.resetMeta {:instrument/always true}))]
             (is (true? (mu.fn/instrument-ns? n)))))))))
 
-(deftest instrumentation-can-be-omitted
+(deftest ^:parallel instrumentation-can-be-omitted
   (testing "omission in macroexpansion"
     (testing "returns a simple fn*"
-      (with-redefs [mu.fn/instrument-ns? (constantly false)]
+      (mt/with-dynamic-redefs [mu.fn/instrument-ns? (constantly false)]
         (let [expansion (macroexpand `(mu.fn/fn :- :int [] "foo"))]
           (is (= expansion '(fn* ([] "foo")))))))
     (testing "returns an instrumented fn"
-      (with-redefs [mu.fn/instrument-ns? (constantly true)]
+      (mt/with-dynamic-redefs [mu.fn/instrument-ns? (constantly true)]
         (let [expansion (macroexpand `(mu.fn/fn :- :int [] "foo"))]
           (is (= (take 2 expansion)
                  '(let* [&f (clojure.core/fn [] "foo")]
@@ -271,7 +271,7 @@
            (catch Exception e
              (is (=? {:type ::mu.fn/invalid-output} (ex-data e)))))))
   (testing "when instrument-ns? returns false, unvalidated form is emitted"
-    (with-redefs [mu.fn/instrument-ns? (constantly false)]
+    (mt/with-dynamic-redefs [mu.fn/instrument-ns? (constantly false)]
       ;; we have to use eval here because `mu.fn/fn` is expanded at _read_ time and we want to change the
       ;; expansion via [[mu.fn/instrument-ns?]]. So that's why we call eval here. Could definitely use some
       ;; macroexpansion tests as well.