diff --git a/modules/cache/cache.go b/modules/cache/cache.go
index 0753671158..b5400b0bd6 100644
--- a/modules/cache/cache.go
+++ b/modules/cache/cache.go
@@ -4,6 +4,7 @@
 package cache
 
 import (
+	"fmt"
 	"strconv"
 	"time"
 
@@ -35,6 +36,37 @@ func Init() error {
 	return nil
 }
 
+const (
+	testCacheKey       = "DefaultCache.TestKey"
+	SlowCacheThreshold = 100 * time.Microsecond
+)
+
+func Test() (time.Duration, error) {
+	if defaultCache == nil {
+		return 0, fmt.Errorf("default cache not initialized")
+	}
+
+	testData := fmt.Sprintf("%x", make([]byte, 500))
+
+	start := time.Now()
+
+	if err := defaultCache.Delete(testCacheKey); err != nil {
+		return 0, fmt.Errorf("expect cache to delete data based on key if exist but got: %w", err)
+	}
+	if err := defaultCache.Put(testCacheKey, testData, 10); err != nil {
+		return 0, fmt.Errorf("expect cache to store data but got: %w", err)
+	}
+	testVal, hit := defaultCache.Get(testCacheKey)
+	if !hit {
+		return 0, fmt.Errorf("expect cache hit but got none")
+	}
+	if testVal != testData {
+		return 0, fmt.Errorf("expect cache to return same value as stored but got other")
+	}
+
+	return time.Since(start), nil
+}
+
 // GetCache returns the currently configured cache
 func GetCache() StringCache {
 	return defaultCache
diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go
index 0c68cc26ee..e0b82f86f2 100644
--- a/modules/cache/cache_test.go
+++ b/modules/cache/cache_test.go
@@ -34,6 +34,18 @@ func TestNewContext(t *testing.T) {
 	assert.Nil(t, con)
 }
 
+func TestTest(t *testing.T) {
+	defaultCache = nil
+	_, err := Test()
+	assert.Error(t, err)
+
+	createTestCache()
+	elapsed, err := Test()
+	assert.NoError(t, err)
+	// mem cache should take from 300ns up to 1ms on modern hardware ...
+	assert.Less(t, elapsed, SlowCacheThreshold)
+}
+
 func TestGetCache(t *testing.T) {
 	createTestCache()
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index fbada5472c..815cba6eec 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -93,6 +93,7 @@ remove_all = Remove All
 remove_label_str = Remove item "%s"
 edit = Edit
 view = View
+test = Test
 
 enabled = Enabled
 disabled = Disabled
@@ -3225,6 +3226,10 @@ config.cache_adapter = Cache Adapter
 config.cache_interval = Cache Interval
 config.cache_conn = Cache Connection
 config.cache_item_ttl = Cache Item TTL
+config.cache_test = Test Cache
+config.cache_test_failed = Failed to probe the cache: %v.
+config.cache_test_slow = Cache test successful, but response is slow: %s.
+config.cache_test_succeeded = Cache test successful, got a response in %s.
 
 config.session_config = Session Configuration
 config.session_provider = Session Provider
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index dee1650b5a..6fc97c949e 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -15,6 +15,7 @@ import (
 	activities_model "code.gitea.io/gitea/models/activities"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/httplib"
 	"code.gitea.io/gitea/modules/json"
@@ -222,6 +223,14 @@ func SelfCheck(ctx *context.Context) {
 
 		ctx.Data["DatabaseCheckHasProblems"] = hasProblem
 	}
+
+	elapsed, err := cache.Test()
+	if err != nil {
+		ctx.Data["CacheError"] = err
+	} else if elapsed > cache.SlowCacheThreshold {
+		ctx.Data["CacheSlow"] = fmt.Sprint(elapsed)
+	}
+
 	ctx.HTML(http.StatusOK, tplSelfCheck)
 }
 
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index 2a842cff82..2ae93e9cac 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -12,6 +12,7 @@ import (
 
 	system_model "code.gitea.io/gitea/models/system"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
@@ -42,6 +43,22 @@ func SendTestMail(ctx *context.Context) {
 	ctx.Redirect(setting.AppSubURL + "/admin/config")
 }
 
+// TestCache test the cache settings
+func TestCache(ctx *context.Context) {
+	elapsed, err := cache.Test()
+	if err != nil {
+		ctx.Flash.Error(ctx.Tr("admin.config.cache_test_failed", err))
+	} else {
+		if elapsed > cache.SlowCacheThreshold {
+			ctx.Flash.Warning(ctx.Tr("admin.config.cache_test_slow", elapsed))
+		} else {
+			ctx.Flash.Info(ctx.Tr("admin.config.cache_test_succeeded", elapsed))
+		}
+	}
+
+	ctx.Redirect(setting.AppSubURL + "/admin/config")
+}
+
 func shadowPasswordKV(cfgItem, splitter string) string {
 	fields := strings.Split(cfgItem, splitter)
 	for i := 0; i < len(fields); i++ {
diff --git a/routers/web/web.go b/routers/web/web.go
index 5fb1ce0e80..08f5d3d068 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -692,6 +692,7 @@ func registerRoutes(m *web.Route) {
 			m.Get("", admin.Config)
 			m.Post("", admin.ChangeConfig)
 			m.Post("/test_mail", admin.SendTestMail)
+			m.Post("/test_cache", admin.TestCache)
 			m.Get("/settings", admin.ConfigSettings)
 		})
 
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 197a6c6add..87f18192a6 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -229,8 +229,8 @@
 					<dt>{{ctx.Locale.Tr "admin.config.mailer_user"}}</dt>
 					<dd>{{if .Mailer.User}}{{.Mailer.User}}{{else}}(empty){{end}}</dd>
 					<div class="divider"></div>
-					<dt class="tw-py-1">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
-					<dd>
+					<dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.send_test_mail"}}</dt>
+					<dd class="tw-py-0">
 						<form class="ui form ignore-dirty" action="{{AppSubUrl}}/admin/config/test_mail" method="post">
 							{{.CsrfTokenHtml}}
 							<div class="ui tiny input">
@@ -260,6 +260,14 @@
 					<dt>{{ctx.Locale.Tr "admin.config.cache_item_ttl"}}</dt>
 					<dd><code>{{.CacheItemTTL}}</code></dd>
 				{{end}}
+				<div class="divider"></div>
+				<dt class="tw-py-1 tw-flex tw-items-center">{{ctx.Locale.Tr "admin.config.cache_test"}}</dt>
+				<dd class="tw-py-0">
+					<form class="ui form ignore-dirty" action="{{AppSubUrl}}/admin/config/test_cache" method="post">
+						{{.CsrfTokenHtml}}
+						<button class="ui tiny primary button">{{ctx.Locale.Tr "test"}}</button>
+					</form>
+				</dd>
 			</dl>
 		</div>
 
diff --git a/templates/admin/self_check.tmpl b/templates/admin/self_check.tmpl
index b249bf228e..a7f43f4e12 100644
--- a/templates/admin/self_check.tmpl
+++ b/templates/admin/self_check.tmpl
@@ -17,32 +17,40 @@
 	<div class="ui attached segment tw-hidden self-check-problem" id="self-check-by-frontend"></div>
 
 	{{if .DatabaseCheckHasProblems}}
-	<div class="ui attached segment self-check-problem">
-		{{if .DatabaseType.IsMySQL}}
-			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
-		{{else if .DatabaseType.IsMSSQL}}
-			<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
-		{{end}}
-		{{if .DatabaseCheckCollationMismatch}}
-			<div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div>
-		{{end}}
-		{{if .DatabaseCheckCollationCaseInsensitive}}
-			<div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div>
-		{{end}}
-		{{if .DatabaseCheckInconsistentCollationColumns}}
-			<div class="ui red message">
-				<details>
-					<summary>{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}</summary>
-					<ul class="tw-w-full">
-					{{range .DatabaseCheckInconsistentCollationColumns}}
-						<li>{{.}}</li>
-					{{end}}
-					</ul>
-				</details>
-			</div>
-		{{end}}
-	</div>
+		<div class="ui attached segment self-check-problem">
+			{{if .DatabaseType.IsMySQL}}
+				<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mysql"}}</div>
+			{{else if .DatabaseType.IsMSSQL}}
+				<div class="tw-p-2">{{ctx.Locale.Tr "admin.self_check.database_fix_mssql"}}</div>
+			{{end}}
+			{{if .DatabaseCheckCollationMismatch}}
+				<div class="ui red message">{{ctx.Locale.Tr "admin.self_check.database_collation_mismatch" .DatabaseCheckResult.ExpectedCollation}}</div>
+			{{end}}
+			{{if .DatabaseCheckCollationCaseInsensitive}}
+				<div class="ui warning message">{{ctx.Locale.Tr "admin.self_check.database_collation_case_insensitive" .DatabaseCheckResult.DatabaseCollation}}</div>
+			{{end}}
+			{{if .DatabaseCheckInconsistentCollationColumns}}
+				<div class="ui red message">
+					<details>
+						<summary>{{ctx.Locale.Tr "admin.self_check.database_inconsistent_collation_columns" .DatabaseCheckResult.DatabaseCollation}}</summary>
+						<ul class="tw-w-full">
+						{{range .DatabaseCheckInconsistentCollationColumns}}
+							<li>{{.}}</li>
+						{{end}}
+						</ul>
+					</details>
+				</div>
+			{{end}}
+		</div>
 	{{end}}
+
+	{{if .CacheError}}
+		<div class="ui red message">{{ctx.Locale.Tr "admin.config.cache_test_failed" .CacheError}}</div>
+	{{end}}
+	{{if .CacheSlow}}
+		<div class="ui warning message">{{ctx.Locale.Tr "admin.config.cache_test_slow" .CacheSlow}}</div>
+	{{end}}
+
 	{{/* only shown when there is no visible "self-check-problem" */}}
 	<div class="ui attached segment tw-hidden self-check-no-problem">
 		{{ctx.Locale.Tr "admin.self_check.no_problem_found"}}