diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index ce1c45ce22e9b0754755729e6d987ae4cde331c1..90aecc99ba92f12fd683ff77e0ded82601d6dbca 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -481,18 +481,19 @@
         db-is-set-up? (or (requiring-resolve 'metabase.db/db-is-set-up?)
                           ;; this should never be hit. it is just overly cautious against a NPE here. But no way this
                           ;; cannot resolve
-                          (constantly false))]
+                          (constantly false))
+        db-value      #(t2/select-one-fn :value Setting :key (setting-name setting-definition-or-name))]
     ;; cannot use db (and cache populated from db) if db is not set up
     (when (and (db-is-set-up?) (allows-site-wide-values? setting))
       (let [v (if *disable-cache*
-                (t2/select-one-fn :value Setting :key (setting-name setting-definition-or-name))
+                (db-value)
                 (do
                   (setting.cache/restore-cache-if-needed!)
                   (let [cache (setting.cache/cache)]
                     (if (nil? cache)
                       ;; If another thread is populating the cache for the first time, we will have a nil value for
                       ;; the cache and must hit the db while the cache populates
-                      (t2/select-one-fn :value Setting :key (setting-name setting-definition-or-name))
+                      (db-value)
                       (clojure.core/get cache (setting-name setting-definition-or-name))))))]
         (not-empty v)))))
 
@@ -615,12 +616,10 @@
   if any."
   [setting-definition-or-name]
   (let [{:keys [cache? getter enabled? default]} (resolve-setting setting-definition-or-name)
-        disable-cache?                           (not cache?)]
+        disable-cache?                           (or *disable-cache* (not cache?))]
     (if (or (nil? enabled?) (enabled?))
-      (if (= *disable-cache* disable-cache?)
-        (getter)
-        (binding [*disable-cache* disable-cache?]
-          (getter)))
+      (binding [*disable-cache* disable-cache?]
+        (getter))
       default)))
 
 
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index ec880a91ed09069b222dc6a51c4f22ccc7c94f0b..42288f4b888c8847cb11910dd2f425e134dae28b 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -201,6 +201,10 @@
     (application-name-for-setting-descriptions))
   :default    "en"
   :visibility :public
+  :getter     (fn []
+                (let [value (setting/get-value-of-type :string :site-locale)]
+                  (when (i18n/available-locale? value)
+                    value)))
   :setter     (fn [new-value]
                 (when new-value
                   (when-not (i18n/available-locale? new-value)
diff --git a/src/metabase/util/i18n/impl.clj b/src/metabase/util/i18n/impl.clj
index 62c4b34d3ad90a5216e94efba6e968dc85826ca9..743eb9bcf86269f3bfa86ecb21c4300c7342584f 100644
--- a/src/metabase/util/i18n/impl.clj
+++ b/src/metabase/util/i18n/impl.clj
@@ -203,31 +203,25 @@
              (log/errorf e "Invalid format string %s" (pr-str format-string))
              format-string)))))))
 
-;; We can't fetch the system locale until the application DB has been initiailized. Once that's done, we don't need to
-;; do the check anymore -- swapping out the getter fn with the simpler one speeds things up substantially
-(def ^:private site-locale-from-setting-fn
-  (atom
-   (fn []
-     (when-let [db-is-set-up? (resolve 'metabase.db/db-is-set-up?)]
-       (when (and (bound? db-is-set-up?)
-                  (db-is-set-up?))
-         (when-let [get-value-of-type (resolve 'metabase.models.setting/get-value-of-type)]
-           (when (bound? get-value-of-type)
-             (let [f (fn [] (get-value-of-type :string :site-locale))]
-               (reset! site-locale-from-setting-fn f)
-               (f)))))))))
+(def ^:private ^:dynamic *in-site-locale-from-setting*
+  "Whether we're currently inside a call to [[site-locale-from-setting]], so we can prevent infinite recursion."
+  false)
 
 (defn site-locale-from-setting
-  "Fetch the value of the `site-locale` Setting.
-  When metabase is shutting down, we need to log some messages after the db connection is closed, so we keep around a
-  cached-site-locale for that purpose."
+  "Fetch the value of the `site-locale` Setting, or `nil` if it is unset."
   []
-  (let [cached-site-locale (atom "en")]
-    (try
-      (let [site-locale (@site-locale-from-setting-fn)]
-        (reset! cached-site-locale site-locale)
-        site-locale)
-      (catch Exception _ @cached-site-locale))))
+  (when-let [get-value-of-type (resolve 'metabase.models.setting/get-value-of-type)]
+    (when (bound? get-value-of-type)
+      ;; make sure we don't try to recursively fetch the site locale when we're actively in the process of fetching it,
+      ;; otherwise that will cause infinite loops if we try to log anything... see #32376
+      (when-not *in-site-locale-from-setting*
+        (binding [*in-site-locale-from-setting* true]
+          ;; if there is an error fetching the Setting, e.g. if the app DB is in the process of shutting down, then just
+          ;; return nil.
+          (try
+            (get-value-of-type :string :site-locale)
+            (catch Exception _
+              nil)))))))
 
 (defmethod print-method Locale
   [locale ^java.io.Writer writer]
diff --git a/test/metabase/cmd/dump_to_h2_test.clj b/test/metabase/cmd/dump_to_h2_test.clj
index c0ef21b3de574195d56c3b6fd0266355aa617d3b..7300b08a8d8c0df3603acb7cfdd106a98cb4cf40 100644
--- a/test/metabase/cmd/dump_to_h2_test.clj
+++ b/test/metabase/cmd/dump_to_h2_test.clj
@@ -71,7 +71,7 @@
                           h2-file-enc         (format "out-%s.db" (mt/random-name))
                           h2-file-default-enc (format "out-%s.db" (mt/random-name))]
         (mt/test-drivers #{:h2 :postgres :mysql}
-          (with-redefs [i18n.impl/site-locale-from-setting-fn (atom (constantly false))]
+          (with-redefs [i18n.impl/site-locale-from-setting (constantly nil)]
             (binding [setting/*disable-cache*         true
                       mdb.connection/*application-db* (mdb.connection/application-db
                                                        driver/*driver*
diff --git a/test/metabase/cmd/load_and_dump_test.clj b/test/metabase/cmd/load_and_dump_test.clj
index 6570a2c9df72de7059014e5bcb311a68f661f651..f344c68e0f8e4305c65f60aa6fe19e0f0e40afcc 100644
--- a/test/metabase/cmd/load_and_dump_test.clj
+++ b/test/metabase/cmd/load_and_dump_test.clj
@@ -38,7 +38,7 @@
                                (mdb.spec/spec driver/*driver* details))))]
           (binding [setting/*disable-cache*         true
                     mdb.connection/*application-db* (mdb.connection/application-db driver/*driver* data-source)]
-            (with-redefs [i18n.impl/site-locale-from-setting-fn (atom (constantly false))]
+            (with-redefs [i18n.impl/site-locale-from-setting (constantly nil)]
               (when-not (= driver/*driver* :h2)
                 (tx/create-db! driver/*driver* {:database-name db-name}))
               (load-from-h2/load-from-h2! h2-fixture-db-file)
diff --git a/test/metabase/public_settings_test.clj b/test/metabase/public_settings_test.clj
index de14f8a50d0fbe8de0a0b54d7c3cbb31919dc585..4202873e9bf77d314f3603d956993b65221a2874 100644
--- a/test/metabase/public_settings_test.clj
+++ b/test/metabase/public_settings_test.clj
@@ -127,41 +127,44 @@
            #"Values greater than 204,800 \(200\.0 MB\) are not allowed"
            (public-settings/query-caching-max-kb! (* 1024 1024)))))))
 
-(deftest site-locale-test
-  (testing "site-locale Setting"
-    (testing "should validate input"
-      (testing "invalid format"
-        (testing "blank string"
-          (mt/with-temporary-setting-values [site-locale "en_US"]
-            (is (thrown-with-msg?
-                 clojure.lang.ExceptionInfo
-                 #"Invalid locale \"\""
-                 (public-settings/site-locale! "")))
-            (is (= "en_US"
-                   (public-settings/site-locale)))))
-
-        (testing "non-existant locale"
-          (mt/with-temporary-setting-values [site-locale "en_US"]
-            (is (thrown-with-msg?
-                 clojure.lang.ExceptionInfo
-                 #"Invalid locale \"en_EN\""
-                 (public-settings/site-locale! "en_EN")))
-            (is (= "en_US"
-                   (public-settings/site-locale)))))))
-
-    (testing "should normalize input"
-      (mt/discard-setting-changes [site-locale]
-        (public-settings/site-locale! "en-us")
+(deftest site-locale-validate-input-test
+  (testing "site-locale should validate input"
+    (testing "blank string"
+      (mt/with-temporary-setting-values [site-locale "en_US"]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"Invalid locale \"\""
+             (public-settings/site-locale! "")))
         (is (= "en_US"
                (public-settings/site-locale)))))
-
-    (testing "should be able to unset site locale"
-      (mt/discard-setting-changes [site-locale]
-        (public-settings/site-locale! "es")
-        (public-settings/site-locale! nil)
-        (is (= "en"
-               (public-settings/site-locale))
-            "should default to English")))))
+    (testing "non-existant locale"
+      (mt/with-temporary-setting-values [site-locale "en_US"]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #"Invalid locale \"en_EN\""
+             (public-settings/site-locale! "en_EN")))
+        (is (= "en_US"
+               (public-settings/site-locale)))))))
+
+(deftest site-locale-normalize-input-test
+  (testing "site-locale should normalize input"
+    (mt/discard-setting-changes [site-locale]
+      (public-settings/site-locale! "en-us")
+      (is (= "en_US"
+             (public-settings/site-locale))))))
+
+(deftest unset-site-locale-test
+  (testing "should be able to unset site-locale"
+    (mt/discard-setting-changes [site-locale]
+      (public-settings/site-locale! "es")
+      (public-settings/site-locale! nil)
+      (is (= "en"
+             (public-settings/site-locale))
+          "should default to English"))))
+
+(deftest site-locale-only-return-valid-locales-test
+  (mt/with-temporary-raw-setting-values [site-locale "wow_this_in_not_a_locale"]
+    (is (nil? (public-settings/site-locale)))))
 
 (deftest redirect-all-requests-to-https-test
   (testing "Shouldn't be allowed to set `redirect-all-requests-to-https` to `true` unless `site-url` is HTTPS"
diff --git a/test/metabase/util/i18n/impl_test.clj b/test/metabase/util/i18n/impl_test.clj
index eddf455476688dd6ef44d28f67d79fa45c2bb2d9..ec60aa51e6a83d236f36c6db4e4cbd61e59ebb38 100644
--- a/test/metabase/util/i18n/impl_test.clj
+++ b/test/metabase/util/i18n/impl_test.clj
@@ -1,7 +1,12 @@
 (ns ^:mb/once metabase.util.i18n.impl-test
   (:require
    [clojure.test :refer :all]
+   [metabase.models.setting :as setting]
+   [metabase.public-settings :as public-settings]
    [metabase.test :as mt]
+   [metabase.util.encryption :as encryption]
+   [metabase.util.encryption-test :as encryption-test]
+   [metabase.util.i18n :as i18n]
    [metabase.util.i18n.impl :as i18n.impl])
   (:import
    (java.util Locale)))
@@ -117,3 +122,32 @@
     (testing "if the original format string is busted, should just return format-string as-is (better than nothing)"
       (is (= "Bad original {a}"
              (i18n.impl/translate "ba-DD" "Bad original {a}" [100]))))))
+
+(deftest avoid-infinite-i18n-loops-test
+  (testing "recursive calls to site-locale should not result in infinite loops (#32376)"
+    (mt/discard-setting-changes [site-locale]
+      (encryption-test/with-secret-key "secret_key__1"
+        ;; set `site-locale` to something encrypted with the first encryption key.
+        (mt/with-temporary-setting-values [site-locale "en"]
+          (binding [setting/*disable-cache* true]
+            (is (= "en"
+                   (i18n.impl/site-locale-from-setting)))
+            ;; rotate the encryption key, which will trigger an error being logged
+            ;; in [[metabase.util.encryption/maybe-decrypt]]... this will cause a Stack Overflow if `log/error` tries to
+            ;; access `:site-locale` recursively to log the message.
+            (encryption-test/with-secret-key "secret_key__2"
+              (testing "low-level functions should return the encrypted String since we can't successfully decrypt it"
+                ;; not 100% sure this general behavior makes sense for values that we cannot decrypt, but invalid
+                ;; locales are handled by the high-level functions below.
+                (is (encryption/possibly-encrypted-string? (#'setting/db-or-cache-value :site-locale))
+                    `setting/db-or-cache-value)
+                (is (encryption/possibly-encrypted-string? (i18n.impl/site-locale-from-setting))
+                    `i18n.impl/site-locale-from-setting))
+              (testing "since the encrypted string is an invalid value for a Locale, high-level functions should return nil"
+                (is (nil? (i18n/site-locale))
+                    `i18n/site-locale)
+                (is (nil? (public-settings/site-locale))
+                    `public-settings/site-locale))
+              (testing "we should still be able to (no-op) i18n stuff"
+                (is (= "Testing"
+                       (i18n/trs "Testing")))))))))))