diff --git a/flake.lock b/flake.lock
index b14ba48..c74fe33 100644
--- a/flake.lock
+++ b/flake.lock
@@ -19,11 +19,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1714030708,
-        "narHash": "sha256-JOGPOxa8N6ySzB7SQBsh0OVz+UXZriyahgvfNHMIY0Y=",
+        "lastModified": 1716715802,
+        "narHash": "sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "b0d52b31f7f4d80f8bf38f0253652125579c35ff",
+        "rev": "e2dd4e18cc1c7314e24154331bae07df76eb582f",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index f981ed1..61f3b55 100644
--- a/flake.nix
+++ b/flake.nix
@@ -16,6 +16,7 @@
           gcc
           go
           gofumpt
+          golangci-lint
           gopls
           gotools
           go-tools
diff --git a/go.mod b/go.mod
index 518dff0..bb3a05a 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ require (
 	github.com/creasty/defaults v1.7.0
 	github.com/go-acme/lego/v4 v4.5.3
 	github.com/go-sql-driver/mysql v1.6.0
+	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/joho/godotenv v1.4.0
 	github.com/lib/pq v1.10.7
 	github.com/mattn/go-sqlite3 v1.14.16
diff --git a/go.sum b/go.sum
index ae24fc3..5875472 100644
--- a/go.sum
+++ b/go.sum
@@ -323,6 +323,8 @@ github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go
index 67219dd..ff34775 100644
--- a/server/certificates/certificates.go
+++ b/server/certificates/certificates.go
@@ -14,6 +14,7 @@ import (
 	"github.com/go-acme/lego/v4/certificate"
 	"github.com/go-acme/lego/v4/challenge/tlsalpn01"
 	"github.com/go-acme/lego/v4/lego"
+	"github.com/hashicorp/golang-lru/v2/expirable"
 	"github.com/reugn/equalizer"
 	"github.com/rs/zerolog/log"
 
@@ -31,11 +32,14 @@ func TLSConfig(mainDomainSuffix string,
 	giteaClient *gitea.Client,
 	acmeClient *AcmeClient,
 	firstDefaultBranch string,
-	keyCache, challengeCache, dnsLookupCache, canonicalDomainCache cache.ICache,
+	challengeCache, canonicalDomainCache cache.ICache,
 	certDB database.CertDB,
 	noDNS01 bool,
 	rawDomain string,
 ) *tls.Config {
+	// every cert is at most 24h in the cache and 7 days before expiry the cert is renewed
+	keyCache := expirable.NewLRU[string, *tls.Certificate](32, nil, 24*time.Hour)
+
 	return &tls.Config{
 		// check DNS name & get certificate from Let's Encrypt
 		GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
@@ -86,7 +90,7 @@ func TLSConfig(mainDomainSuffix string,
 				}
 			} else {
 				var targetRepo, targetBranch string
-				targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
+				targetOwner, targetRepo, targetBranch = dnsutils.GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch)
 				if targetOwner == "" {
 					// DNS not set up, return main certificate to redirect to the docs
 					domain = mainDomainSuffix
@@ -107,7 +111,7 @@ func TLSConfig(mainDomainSuffix string,
 
 			if tlsCertificate, ok := keyCache.Get(domain); ok {
 				// we can use an existing certificate object
-				return tlsCertificate.(*tls.Certificate), nil
+				return tlsCertificate, nil
 			}
 
 			var tlsCertificate *tls.Certificate
@@ -132,9 +136,8 @@ func TLSConfig(mainDomainSuffix string,
 				}
 			}
 
-			if err := keyCache.Set(domain, tlsCertificate, 15*time.Minute); err != nil {
-				return nil, err
-			}
+			keyCache.Add(domain, tlsCertificate)
+
 			return tlsCertificate, nil
 		},
 		NextProtos: []string{
@@ -186,11 +189,10 @@ func (c *AcmeClient) retrieveCertFromDB(sni, mainDomainSuffix string, useDnsProv
 
 	// TODO: document & put into own function
 	if !strings.EqualFold(sni, mainDomainSuffix) {
-		tlsCertificate.Leaf, err = x509.ParseCertificate(tlsCertificate.Certificate[0])
+		tlsCertificate.Leaf, err = leaf(&tlsCertificate)
 		if err != nil {
-			return nil, fmt.Errorf("error parsing leaf tlsCert: %w", err)
+			return nil, err
 		}
-
 		// renew certificates 7 days before they expire
 		if tlsCertificate.Leaf.NotAfter.Before(time.Now().Add(7 * 24 * time.Hour)) {
 			// TODO: use ValidTill of custom cert struct
@@ -291,6 +293,7 @@ func (c *AcmeClient) obtainCert(acmeClient *lego.Client, domains []string, renew
 			}
 			leaf, err := leaf(&tlsCertificate)
 			if err == nil && leaf.NotAfter.After(time.Now()) {
+				tlsCertificate.Leaf = leaf
 				// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at
 				renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10))
 				if err := keyDatabase.Put(name, renew); err != nil {
@@ -388,11 +391,20 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, acmeClient *Acm
 	}
 }
 
-// leaf returns the parsed leaf certificate, either from c.leaf or by parsing
+// leaf returns the parsed leaf certificate, either from c.Leaf or by parsing
 // the corresponding c.Certificate[0].
+// After successfully parsing the cert c.Leaf gets set to the parsed cert.
 func leaf(c *tls.Certificate) (*x509.Certificate, error) {
 	if c.Leaf != nil {
 		return c.Leaf, nil
 	}
-	return x509.ParseCertificate(c.Certificate[0])
+
+	leaf, err := x509.ParseCertificate(c.Certificate[0])
+	if err != nil {
+		return nil, fmt.Errorf("tlsCert - failed to parse leaf: %w", err)
+	}
+
+	c.Leaf = leaf
+
+	return leaf, err
 }
diff --git a/server/dns/dns.go b/server/dns/dns.go
index 970f0c0..e29e42c 100644
--- a/server/dns/dns.go
+++ b/server/dns/dns.go
@@ -5,22 +5,26 @@ import (
 	"strings"
 	"time"
 
-	"codeberg.org/codeberg/pages/server/cache"
+	"github.com/hashicorp/golang-lru/v2/expirable"
 )
 
-// lookupCacheTimeout specifies the timeout for the DNS lookup cache.
-var lookupCacheTimeout = 15 * time.Minute
+const (
+	lookupCacheValidity = 30 * time.Second
+	defaultPagesRepo    = "pages"
+)
 
-var defaultPagesRepo = "pages"
+// TODO(#316): refactor to not use global variables
+var lookupCache *expirable.LRU[string, string] = expirable.NewLRU[string, string](4096, nil, lookupCacheValidity)
 
 // GetTargetFromDNS searches for CNAME or TXT entries on the request domain ending with MainDomainSuffix.
 // If everything is fine, it returns the target data.
-func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLookupCache cache.ICache) (targetOwner, targetRepo, targetBranch string) {
+func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string) (targetOwner, targetRepo, targetBranch string) {
 	// Get CNAME or TXT
 	var cname string
 	var err error
-	if cachedName, ok := dnsLookupCache.Get(domain); ok {
-		cname = cachedName.(string)
+
+	if entry, ok := lookupCache.Get(domain); ok {
+		cname = entry
 	} else {
 		cname, err = net.LookupCNAME(domain)
 		cname = strings.TrimSuffix(cname, ".")
@@ -38,7 +42,7 @@ func GetTargetFromDNS(domain, mainDomainSuffix, firstDefaultBranch string, dnsLo
 				}
 			}
 		}
-		_ = dnsLookupCache.Set(domain, cname, lookupCacheTimeout)
+		_ = lookupCache.Add(domain, cname)
 	}
 	if cname == "" {
 		return
diff --git a/server/handler/handler.go b/server/handler/handler.go
index ffc3400..c038c2d 100644
--- a/server/handler/handler.go
+++ b/server/handler/handler.go
@@ -23,7 +23,7 @@ const (
 func Handler(
 	cfg config.ServerConfig,
 	giteaClient *gitea.Client,
-	dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache,
+	canonicalDomainCache, redirectsCache cache.ICache,
 ) http.HandlerFunc {
 	return func(w http.ResponseWriter, req *http.Request) {
 		log.Debug().Msg("\n----------------------------------------------------------")
@@ -108,7 +108,7 @@ func Handler(
 				trimmedHost,
 				pathElements,
 				cfg.PagesBranches[0],
-				dnsLookupCache, canonicalDomainCache, redirectsCache)
+				canonicalDomainCache, redirectsCache)
 		}
 	}
 }
diff --git a/server/handler/handler_custom_domain.go b/server/handler/handler_custom_domain.go
index 82953f9..852001a 100644
--- a/server/handler/handler_custom_domain.go
+++ b/server/handler/handler_custom_domain.go
@@ -19,10 +19,10 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
 	trimmedHost string,
 	pathElements []string,
 	firstDefaultBranch string,
-	dnsLookupCache, canonicalDomainCache, redirectsCache cache.ICache,
+	canonicalDomainCache, redirectsCache cache.ICache,
 ) {
 	// Serve pages from custom domains
-	targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
+	targetOwner, targetRepo, targetBranch := dns.GetTargetFromDNS(trimmedHost, mainDomainSuffix, firstDefaultBranch)
 	if targetOwner == "" {
 		html.ReturnErrorPage(ctx,
 			"could not obtain repo owner from custom domain",
@@ -53,7 +53,7 @@ func handleCustomDomain(log zerolog.Logger, ctx *context.Context, giteaClient *g
 			return
 		} else if canonicalDomain != trimmedHost {
 			// only redirect if the target is also a codeberg page!
-			targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch, dnsLookupCache)
+			targetOwner, _, _ = dns.GetTargetFromDNS(strings.SplitN(canonicalDomain, "/", 2)[0], mainDomainSuffix, firstDefaultBranch)
 			if targetOwner != "" {
 				ctx.Redirect("https://"+canonicalDomain+"/"+targetOpt.TargetPath, http.StatusTemporaryRedirect)
 				return
diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go
index 0ae7962..765b3b1 100644
--- a/server/handler/handler_test.go
+++ b/server/handler/handler_test.go
@@ -29,7 +29,7 @@ func TestHandlerPerformance(t *testing.T) {
 		AllowedCorsDomains: []string{"raw.codeberg.org", "fonts.codeberg.org", "design.codeberg.org"},
 		PagesBranches:      []string{"pages"},
 	}
-	testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache(), cache.NewInMemoryCache())
+	testHandler := Handler(serverCfg, giteaClient, cache.NewInMemoryCache(), cache.NewInMemoryCache())
 
 	testCase := func(uri string, status int) {
 		t.Run(uri, func(t *testing.T) {
diff --git a/server/startup.go b/server/startup.go
index 95c3c5c..6642d83 100644
--- a/server/startup.go
+++ b/server/startup.go
@@ -66,12 +66,9 @@ func Serve(ctx *cli.Context) error {
 	}
 	defer closeFn()
 
-	keyCache := cache.NewInMemoryCache()
 	challengeCache := cache.NewInMemoryCache()
 	// canonicalDomainCache stores canonical domains
 	canonicalDomainCache := cache.NewInMemoryCache()
-	// dnsLookupCache stores DNS lookups for custom domains
-	dnsLookupCache := cache.NewInMemoryCache()
 	// redirectsCache stores redirects in _redirects files
 	redirectsCache := cache.NewInMemoryCache()
 	// clientResponseCache stores responses from the Gitea server
@@ -104,7 +101,7 @@ func Serve(ctx *cli.Context) error {
 		giteaClient,
 		acmeClient,
 		cfg.Server.PagesBranches[0],
-		keyCache, challengeCache, dnsLookupCache, canonicalDomainCache,
+		challengeCache, canonicalDomainCache,
 		certDB,
 		cfg.ACME.NoDNS01,
 		cfg.Server.RawDomain,
@@ -134,7 +131,7 @@ func Serve(ctx *cli.Context) error {
 	}
 
 	// Create ssl handler based on settings
-	sslHandler := handler.Handler(cfg.Server, giteaClient, dnsLookupCache, canonicalDomainCache, redirectsCache)
+	sslHandler := handler.Handler(cfg.Server, giteaClient, canonicalDomainCache, redirectsCache)
 
 	// Start the ssl listener
 	log.Info().Msgf("Start SSL server using TCP listener on %s", listener.Addr())