diff --git a/.gitignore b/.gitignore
index 32df7b0c..4faf1961 100644
--- a/.gitignore
+++ b/.gitignore
@@ -163,3 +163,4 @@ local.properties
 *.terraform.lock.hcl
 .terraform/
 *.tfplan
+src/airsonic-advanced/applicationinsights.log
diff --git a/src/airsonic-advanced/airsonic-main/pom.xml b/src/airsonic-advanced/airsonic-main/pom.xml
index f415b793..13a8aae3 100644
--- a/src/airsonic-advanced/airsonic-main/pom.xml
+++ b/src/airsonic-advanced/airsonic-main/pom.xml
@@ -100,12 +100,12 @@
             <artifactId>spring-boot-starter-cache</artifactId>
         </dependency>
         <dependency>
-            <groupId>javax.cache</groupId>
-            <artifactId>cache-api</artifactId>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.ehcache</groupId>
-            <artifactId>ehcache</artifactId>
+            <groupId>javax.cache</groupId>
+            <artifactId>cache-api</artifactId>
         </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/Application.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/Application.java
index 9b834ae2..14e8f3bf 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/Application.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/Application.java
@@ -1,6 +1,7 @@
 package org.airsonic.player;
 
 import com.microsoft.applicationinsights.attach.ApplicationInsights;
+import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigCustomizer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.boot.WebApplicationType;
@@ -12,6 +13,7 @@
 import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
 import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
 import org.springframework.context.annotation.AdviceMode;
+import org.springframework.context.annotation.Bean;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 import org.springframework.util.ReflectionUtils;
 
@@ -21,6 +23,7 @@
     JmxAutoConfiguration.class,
     MultipartAutoConfiguration.class // TODO: update to use spring boot builtin multipart support
 })
+
 @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
 public class Application extends SpringBootServletInitializer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
 
@@ -72,4 +75,10 @@ public static void main(String[] args) {
         doConfigure(builder).run(args);
     }
 
+    @Bean
+    public CircuitBreakerConfigCustomizer configureCustomizer() {
+        return CircuitBreakerConfigCustomizer
+                .of("default", builder -> builder.slidingWindowSize(100));
+    }
+
 }
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ApiExceptionHandler.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ApiExceptionHandler.java
new file mode 100644
index 00000000..f54b4a5d
--- /dev/null
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ApiExceptionHandler.java
@@ -0,0 +1,35 @@
+package org.airsonic.player.component;
+
+import io.github.resilience4j.bulkhead.BulkheadFullException;
+import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
+import io.github.resilience4j.ratelimiter.RequestNotPermitted;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.util.concurrent.TimeoutException;
+
+@ControllerAdvice
+public class ApiExceptionHandler {
+
+    @ExceptionHandler({ CallNotPermittedException.class })
+    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
+    public void handleCallNotPermittedException() {
+    }
+
+    @ExceptionHandler({ TimeoutException.class })
+    @ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
+    public void handleTimeoutException() {
+    }
+
+    @ExceptionHandler({ BulkheadFullException.class })
+    @ResponseStatus(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED)
+    public void handleBulkheadFullException() {
+    }
+
+    @ExceptionHandler({ RequestNotPermitted.class })
+    @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
+    public void handleRequestNotPermitted() {
+    }
+}
\ No newline at end of file
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ExternalAPICaller.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ExternalAPICaller.java
new file mode 100644
index 00000000..157d6864
--- /dev/null
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ExternalAPICaller.java
@@ -0,0 +1,256 @@
+package org.airsonic.player.component;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.airsonic.player.domain.Version;
+import org.airsonic.player.util.Util;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.impl.client.AbstractResponseHandler;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.support.PropertiesLoaderUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Component
+public class ExternalAPICaller {
+    private final RestTemplate restTemplate;
+
+    private static int requestCount = 0;
+
+    private static final Logger LOG = LoggerFactory.getLogger(ExternalAPICaller.class);
+
+    private static final String VERSION_URL = "https://api.github.com/repos/airsonic-advanced/airsonic-advanced/releases";
+
+    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneId.of("UTC"));
+
+    private final Properties build;
+
+    private Version localVersion;
+    private Version latestFinalVersion;
+    private Version latestBetaVersion;
+    private Instant localBuildDate;
+    private String localBuildNumber;
+
+    @Autowired
+    public ExternalAPICaller(RestTemplate restTemplate) throws IOException {
+        build = PropertiesLoaderUtils.loadAllProperties("build.properties");
+        this.restTemplate = restTemplate;
+    }
+
+    /**
+     * Returns the version number for the locally installed Airsonic version.
+     *
+     * @return The version number for the locally installed Airsonic version.
+     */
+    public Version getLocalVersion() {
+        if (localVersion == null) {
+            try {
+                localVersion = new Version(build.getProperty("version") + "." + build.getProperty("timestamp"));
+                LOG.info("Resolved local Airsonic version to: {}", localVersion);
+            } catch (Exception x) {
+                LOG.warn("Failed to resolve local Airsonic version.", x);
+            }
+        }
+        return localVersion;
+    }
+        /**
+     * Returns the build date for the locally installed Airsonic version.
+     *
+     * @return The build date for the locally installed Airsonic version, or <code>null</code>
+     *         if the build date can't be resolved.
+     */
+    public Instant getLocalBuildDate() {
+        if (localBuildDate == null) {
+            try {
+                String date = build.getProperty("timestamp");
+                localBuildDate = DATE_FORMAT.parse(date, Instant::from);
+            } catch (Exception x) {
+                LOG.warn("Failed to resolve local Airsonic build date.", x);
+            }
+        }
+        return localBuildDate;
+    }
+
+    /**
+     * Returns the build number for the locally installed Airsonic version.
+     *
+     * @return The build number for the locally installed Airsonic version, or <code>null</code>
+     *         if the build number can't be resolved.
+     */
+    public String getLocalBuildNumber() {
+        if (localBuildNumber == null) {
+            try {
+                localBuildNumber = build.getProperty("revision");
+            } catch (Exception x) {
+                LOG.warn("Failed to resolve local Airsonic build commit.", x);
+            }
+        }
+        return localBuildNumber;
+    }
+
+    public Version callApi() throws IOException {
+        // Check to see if we should be simulating failure. If it is not set, proceed as usual.
+        try {
+            String retryDemo = System.getenv("AIRSONIC_RETRY_DEMO");
+            if (!StringUtils.isEmpty(retryDemo)) {
+                int errorRate = Integer.parseInt(retryDemo);
+                if (requestCount++ % errorRate == 0)
+                    throw new IOException("Simulated exception trying to get version info from GitHub");
+            }
+        } catch (NumberFormatException nfe) {
+            throw new IOException("Simulation started with invalid RETRY_DEMO value", nfe);
+        }
+        LOG.debug("Starting to read latest version");
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setConnectTimeout(10000)
+                .setSocketTimeout(10000)
+                .build();
+        HttpGet method = new HttpGet(VERSION_URL);
+        method.setConfig(requestConfig);
+        method.setHeader("accept", "application/vnd.github.v3+json");
+        method.setHeader("User-Agent", "Airsonic/" + getLocalVersion());
+        List<Map<String, Object>> content;
+        try (CloseableHttpClient client = HttpClients.createDefault()) {
+            content = client.execute(method, respHandler);
+        } catch (ConnectTimeoutException e) {
+            LOG.warn("Got a timeout when trying to reach {}", VERSION_URL);
+            throw e;
+        }
+
+        List<Map<String, Object>> releases = content.stream()
+                .sorted(Comparator.<Map<String, Object>,Instant>comparing(r -> Instant.parse((String) r.get("published_at")), Comparator.reverseOrder()))
+                .collect(Collectors.toList());
+
+        Optional<Map<String, Object>> betaR = releases.stream().findFirst();
+        Optional<Map<String, Object>> finalR = releases.stream().filter(x -> !((Boolean)x.get("draft")) && !((Boolean)x.get("prerelease"))).findFirst();
+        Optional<Map<String,Object>> currentR = releases.stream().filter(x ->
+                StringUtils.equals(build.getProperty("version") + "." + build.getProperty("timestamp"), (String) x.get("tag_name")) ||
+                        StringUtils.equals(build.getProperty("version"), (String) x.get("tag_name"))).findAny();
+
+        LOG.debug("Got {} for beta version", betaR.map(x -> x.get("tag_name")).orElse(null));
+        LOG.debug("Got {} for final version", finalR.map(x -> x.get("tag_name")).orElse(null));
+
+        latestBetaVersion = betaR.map(releaseToVersionMapper).orElse(null);
+        latestFinalVersion = finalR.map(releaseToVersionMapper).orElse(null);
+        localVersion = currentR.map(releaseToVersionMapper).orElse(localVersion);
+
+        return latestBetaVersion;
+    }
+
+    /**
+     * Time when latest version was fetched (in milliseconds).
+     */
+    private long lastVersionFetched;
+
+    /**
+     * Only fetch last version this often (in milliseconds.).
+     */
+    private static final long LAST_VERSION_FETCH_INTERVAL = 7L * 24L * 3600L * 1000L; // One week
+
+    /**
+     * Returns whether a new final version of Airsonic is available.
+     *
+     * @return Whether a new final version of Airsonic is available.
+     */
+    public boolean isNewFinalVersionAvailable() {
+        Version latest = getLatestFinalVersion();
+        Version local = getLocalVersion();
+
+        if (latest == null || local == null) {
+            return false;
+        }
+
+        return local.compareTo(latest) < 0;
+    }
+
+    /**
+     * Returns whether a new beta version of Airsonic is available.
+     *
+     * @return Whether a new beta version of Airsonic is available.
+     */
+    public boolean isNewBetaVersionAvailable() {
+        Version latest = getLatestBetaVersion();
+        Version local = getLocalVersion();
+
+        if (latest == null || local == null) {
+            return false;
+        }
+
+        return local.compareTo(latest) < 0;
+    }
+
+    private static ResponseHandler<List<Map<String, Object>>> respHandler = new AbstractResponseHandler<List<Map<String,Object>>>() {
+        @Override
+        public List<Map<String, Object>> handleEntity(HttpEntity entity) throws IOException {
+            try (InputStream is = entity.getContent(); InputStream bis = new BufferedInputStream(is)) {
+                return Util.getObjectMapper().readValue(bis, new TypeReference<List<Map<String, Object>>>() {});
+            }
+        }
+    };
+
+    private static Function<Map<String,Object>, Version> releaseToVersionMapper = r ->
+            new Version(
+                    (String) r.get("tag_name"),
+                    (String) r.get("target_commitish"),
+                    (Boolean) r.get("draft") || (Boolean) r.get("prerelease"),
+                    (String) r.get("html_url"),
+                    Instant.parse((String) r.get("published_at")),
+                    Instant.parse((String) r.get("created_at")),
+                    (List<Map<String,Object>>) r.get("assets")
+                    );
+
+    /**
+     * Returns the version number for the latest available Airsonic beta version.
+     *
+     * @return The version number for the latest available Airsonic beta version, or <code>null</code>
+     *         if the version number can't be resolved.
+     */
+    public synchronized Version getLatestBetaVersion() {
+        refreshLatestVersion();
+        return latestBetaVersion;
+    }
+
+    /**
+     * Returns the version number for the latest available Airsonic final version.
+     *
+     * @return The version number for the latest available Airsonic final version, or <code>null</code>
+     *         if the version number can't be resolved.
+     */
+    public synchronized Version getLatestFinalVersion() {
+        refreshLatestVersion();
+        return latestFinalVersion;
+    }
+
+    /**
+     * Refreshes the latest final and beta versions.
+     */
+    private void refreshLatestVersion() {
+        try {
+            callApi();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ExternalApiCallerConfig.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ExternalApiCallerConfig.java
new file mode 100644
index 00000000..06b417d5
--- /dev/null
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/component/ExternalApiCallerConfig.java
@@ -0,0 +1,18 @@
+package org.airsonic.player.component;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class ExternalApiCallerConfig {
+    private static final String VERSION_URL = "https://api.github.com/repos/airsonic-advanced/airsonic-advanced/releases";
+
+    @Bean
+    public RestTemplate restTemplate() {
+        return new RestTemplateBuilder()
+                .rootUri(VERSION_URL)
+                .build();
+    }
+}
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java
index d0a6ecac..3bbe3d23 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/MediaFile.java
@@ -19,6 +19,7 @@
  */
 package org.airsonic.player.domain;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import org.apache.commons.io.FilenameUtils;
 
 import java.nio.file.Path;
@@ -175,6 +176,7 @@ public boolean isFile() {
         return mediaType != MediaType.DIRECTORY && mediaType != MediaType.ALBUM;
     }
 
+    @JsonIgnore
     public boolean isAlbum() {
         return mediaType == MediaType.ALBUM;
     }
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/User.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/User.java
index bd17a043..6c6e5b1d 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/User.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/domain/User.java
@@ -19,6 +19,9 @@
  */
 package org.airsonic.player.domain;
 
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import org.airsonic.player.util.Util;
 
 import java.util.Collections;
@@ -43,8 +46,10 @@ public class User {
     private long bytesUploaded;
     private Set<Role> roles;
 
-    public User(String username, String email, boolean ldapAuthenticated,
-            long bytesStreamed, long bytesDownloaded, long bytesUploaded, Set<Role> roles) {
+    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+    public User(@JsonProperty("username")String username, @JsonProperty("email")String email, @JsonProperty("ldapAuthenticated")boolean ldapAuthenticated,
+                @JsonProperty("bytesStreamed")long bytesStreamed, @JsonProperty("bytesDownloaded")long bytesDownloaded, @JsonProperty("bytesUploaded")long bytesUploaded,
+                @JsonProperty("roles")Set<Role> roles) {
         this.username = username;
         this.email = email;
         this.ldapAuthenticated = ldapAuthenticated;
@@ -58,6 +63,7 @@ public User(String username, String email) {
         this(username, email, false, 0, 0, 0, Collections.emptySet());
     }
 
+    @JsonProperty("username")
     public String getUsername() {
         return username;
     }
@@ -102,6 +108,7 @@ public void setBytesUploaded(long bytesUploaded) {
         this.bytesUploaded = bytesUploaded;
     }
 
+    @JsonProperty("roles")
     public Set<Role> getRoles() {
         return roles;
     }
@@ -110,46 +117,57 @@ public void setRoles(Set<Role> roles) {
         this.roles = roles;
     }
 
+    @JsonIgnore
     public boolean isAdminRole() {
         return roles.contains(Role.ADMIN);
     }
 
+    @JsonIgnore
     public boolean isSettingsRole() {
         return roles.contains(Role.SETTINGS);
     }
 
+    @JsonIgnore
     public boolean isCommentRole() {
         return roles.contains(Role.COMMENT);
     }
 
+    @JsonIgnore
     public boolean isDownloadRole() {
         return roles.contains(Role.DOWNLOAD);
     }
 
+    @JsonIgnore
     public boolean isUploadRole() {
         return roles.contains(Role.UPLOAD);
     }
 
+    @JsonIgnore
     public boolean isPlaylistRole() {
         return roles.contains(Role.PLAYLIST);
     }
 
+    @JsonIgnore
     public boolean isCoverArtRole() {
         return roles.contains(Role.COVERART);
     }
 
+    @JsonIgnore
     public boolean isPodcastRole() {
         return roles.contains(Role.PODCAST);
     }
 
+    @JsonIgnore
     public boolean isStreamRole() {
         return roles.contains(Role.STREAM);
     }
 
+    @JsonIgnore
     public boolean isJukeboxRole() {
         return roles.contains(Role.JUKEBOX);
     }
 
+    @JsonIgnore
     public boolean isShareRole() {
         return roles.contains(Role.SHARE);
     }
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java
index 7c888c69..4548b08e 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/CoverArtService.java
@@ -10,7 +10,6 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.annotation.CacheConfig;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import java.nio.file.Path;
@@ -67,7 +66,7 @@ public void persistIfNeeded(Artist artist) {
         }
     }
 
-    @Cacheable(key = "#type.toString().concat('-').concat(#id)", unless = "#result == null") // 'unless' condition should never happen, because of null-object pattern
+    //@Cacheable(key = "#type.toString().concat('-').concat(#id)", unless = "#result == null") // 'unless' condition should never happen, because of null-object pattern
     public CoverArt get(EntityType type, int id) {
         return Optional.ofNullable(coverArtDao.get(type, id)).orElse(CoverArt.NULL_ART);
     }
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java
index 06f58f00..a9b1d1d2 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/MediaFileService.java
@@ -44,7 +44,6 @@
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.Cacheable;
 import org.springframework.cache.annotation.Caching;
 import org.springframework.stereotype.Service;
 
@@ -141,7 +140,7 @@ public MediaFile getMediaFile(String relativePath, Integer folderId, boolean min
         return getMediaFile(Paths.get(relativePath), mediaFolderService.getMusicFolderById(folderId), minimizeDiskAccess);
     }
 
-    @Cacheable(cacheNames = "mediaFilePathCache", key = "#relativePath.toString().concat('-').concat(#folder.id)", condition = "#root.target.memoryCacheEnabled", unless = "#result == null")
+    //@Cacheable(cacheNames = "mediaFilePathCache", key = "#relativePath.toString().concat('-').concat(#folder.id)", condition = "#root.target.memoryCacheEnabled", unless = "#result == null")
     public MediaFile getMediaFile(Path relativePath, MusicFolder folder, boolean minimizeDiskAccess) {
         // Look in database.
         MediaFile result = mediaFileDao.getMediaFile(relativePath.toString(), folder.getId());
@@ -163,7 +162,7 @@ public MediaFile getMediaFile(Path relativePath, MusicFolder folder, boolean min
         return result;
     }
 
-    @Cacheable(cacheNames = "mediaFileIdCache", condition = "#root.target.memoryCacheEnabled", unless = "#result == null")
+    //@Cacheable(cacheNames = "mediaFileIdCache", condition = "#root.target.memoryCacheEnabled", unless = "#result == null")
     public MediaFile getMediaFile(int id) {
         MediaFile mediaFile = mediaFileDao.getMediaFile(id);
         if (mediaFile == null) {
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java
index 31093d8f..9dbfd350 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/PlaylistService.java
@@ -37,7 +37,6 @@
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.Cacheable;
 import org.springframework.messaging.simp.SimpMessagingTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.util.Assert;
@@ -147,12 +146,12 @@ public List<Playlist> getWritablePlaylistsForUser(String username) {
         return playlistDao.getWritablePlaylistsForUser(username);
     }
 
-    @Cacheable(cacheNames = "playlistCache", unless = "#result == null")
+    //@Cacheable(cacheNames = "playlistCache", unless = "#result == null")
     public Playlist getPlaylist(int id) {
         return playlistDao.getPlaylist(id);
     }
 
-    @Cacheable(cacheNames = "playlistUsersCache", unless = "#result == null")
+    //@Cacheable(cacheNames = "playlistUsersCache", unless = "#result == null")
     public List<String> getPlaylistUsers(int playlistId) {
         return playlistDao.getPlaylistUsers(playlistId);
     }
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java
index 16803abe..6fa9b33e 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/SecurityService.java
@@ -36,7 +36,6 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.annotation.CacheConfig;
 import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.Cacheable;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -255,7 +254,7 @@ public User getUserByName(String username) {
      * @param caseSensitive If false, will do a case insensitive search
      * @return The corresponding User
      */
-    @Cacheable(key = "#username", condition = "#caseSensitive", unless = "#result == null")
+    //@Cacheable(key = "#username", condition = "#caseSensitive", unless = "#result == null")
     public User getUserByName(String username, boolean caseSensitive) {
         return userDao.getUserByName(username, caseSensitive);
     }
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/VersionService.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/VersionService.java
index 1c80e9c8..705ef0b4 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/VersionService.java
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/service/VersionService.java
@@ -19,34 +19,18 @@
  */
 package org.airsonic.player.service;
 
-import com.fasterxml.jackson.core.type.TypeReference;
+import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
+import io.github.resilience4j.retry.annotation.Retry;
+import org.airsonic.player.component.ExternalAPICaller;
 import org.airsonic.player.domain.Version;
-import org.airsonic.player.util.Util;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.HttpEntity;
-import org.apache.http.client.ResponseHandler;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.conn.ConnectTimeoutException;
-import org.apache.http.impl.client.AbstractResponseHandler;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.core.io.support.PropertiesLoaderUtils;
-import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
 
-import java.io.*;
+import java.io.IOException;
 import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Properties;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
 /**
  * Provides version-related services, including functionality for determining whether a newer
@@ -54,71 +38,25 @@
  *
  * @author Sindre Mehus
  */
-@Service
+@RestController
 public class VersionService {
-    private static final Logger LOG = LoggerFactory.getLogger(VersionService.class);
-
-    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneId.of("UTC"));
 
-    private final Properties build;
+    private final ExternalAPICaller externalAPICaller;
 
-    private Version localVersion;
-    private Version latestFinalVersion;
-    private Version latestBetaVersion;
-    private Instant localBuildDate;
-    private String localBuildNumber;
+    private static final Logger LOG = LoggerFactory.getLogger(VersionService.class);
 
-    public VersionService() throws IOException {
-        build = PropertiesLoaderUtils.loadAllProperties("build.properties");
+    @Autowired
+    public VersionService(ExternalAPICaller externalApi) throws IOException {
+        this.externalAPICaller = externalApi;
     }
 
-    /**
-     * Time when latest version was fetched (in milliseconds).
-     */
-    private long lastVersionFetched;
-
-    /**
-     * Only fetch last version this often (in milliseconds.).
-     */
-    private static final long LAST_VERSION_FETCH_INTERVAL = 7L * 24L * 3600L * 1000L; // One week
-
     /**
      * Returns the version number for the locally installed Airsonic version.
      *
      * @return The version number for the locally installed Airsonic version.
      */
     public Version getLocalVersion() {
-        if (localVersion == null) {
-            try {
-                localVersion = new Version(build.getProperty("version") + "." + build.getProperty("timestamp"));
-                LOG.info("Resolved local Airsonic version to: {}", localVersion);
-            } catch (Exception x) {
-                LOG.warn("Failed to resolve local Airsonic version.", x);
-            }
-        }
-        return localVersion;
-    }
-
-    /**
-     * Returns the version number for the latest available Airsonic final version.
-     *
-     * @return The version number for the latest available Airsonic final version, or <code>null</code>
-     *         if the version number can't be resolved.
-     */
-    public synchronized Version getLatestFinalVersion() {
-        refreshLatestVersion();
-        return latestFinalVersion;
-    }
-
-    /**
-     * Returns the version number for the latest available Airsonic beta version.
-     *
-     * @return The version number for the latest available Airsonic beta version, or <code>null</code>
-     *         if the version number can't be resolved.
-     */
-    public synchronized Version getLatestBetaVersion() {
-        refreshLatestVersion();
-        return latestBetaVersion;
+        return externalAPICaller.getLocalVersion();
     }
 
     /**
@@ -128,15 +66,7 @@ public synchronized Version getLatestBetaVersion() {
      *         if the build date can't be resolved.
      */
     public Instant getLocalBuildDate() {
-        if (localBuildDate == null) {
-            try {
-                String date = build.getProperty("timestamp");
-                localBuildDate = DATE_FORMAT.parse(date, Instant::from);
-            } catch (Exception x) {
-                LOG.warn("Failed to resolve local Airsonic build date.", x);
-            }
-        }
-        return localBuildDate;
+        return externalAPICaller.getLocalBuildDate();
     }
 
     /**
@@ -146,14 +76,7 @@ public Instant getLocalBuildDate() {
      *         if the build number can't be resolved.
      */
     public String getLocalBuildNumber() {
-        if (localBuildNumber == null) {
-            try {
-                localBuildNumber = build.getProperty("revision");
-            } catch (Exception x) {
-                LOG.warn("Failed to resolve local Airsonic build commit.", x);
-            }
-        }
-        return localBuildNumber;
+        return externalAPICaller.getLocalBuildNumber();
     }
 
     /**
@@ -161,15 +84,11 @@ public String getLocalBuildNumber() {
      *
      * @return Whether a new final version of Airsonic is available.
      */
+    @Retry(name = "retryApi", fallbackMethod = "isNewVersionAvailableFallback")
+    @CircuitBreaker(name = "CircuitBreakerService")
+    @GetMapping("/isNewFinalVersionAvailable")
     public boolean isNewFinalVersionAvailable() {
-        Version latest = getLatestFinalVersion();
-        Version local = getLocalVersion();
-
-        if (latest == null || local == null) {
-            return false;
-        }
-
-        return local.compareTo(latest) < 0;
+        return externalAPICaller.isNewFinalVersionAvailable();
     }
 
     /**
@@ -177,94 +96,53 @@ public boolean isNewFinalVersionAvailable() {
      *
      * @return Whether a new beta version of Airsonic is available.
      */
+    @Retry(name = "retryApi", fallbackMethod = "isNewVersionAvailableFallback")
+    @CircuitBreaker(name = "CircuitBreakerService")
+    @GetMapping("/isNewBetaVersionAvailable")
     public boolean isNewBetaVersionAvailable() {
-        Version latest = getLatestBetaVersion();
-        Version local = getLocalVersion();
-
-        if (latest == null || local == null) {
-            return false;
-        }
-
-        return local.compareTo(latest) < 0;
+        return externalAPICaller.isNewBetaVersionAvailable();
     }
 
     /**
-     * Refreshes the latest final and beta versions.
+     * Returns the version number for the latest available Airsonic beta version.
+     *
+     * @return The version number for the latest available Airsonic beta version, or <code>null</code>
+     *         if the version number can't be resolved.
      */
-    private void refreshLatestVersion() {
-        long now = System.currentTimeMillis();
-        boolean isOutdated = now - lastVersionFetched > LAST_VERSION_FETCH_INTERVAL;
-
-        if (isOutdated) {
-            try {
-                lastVersionFetched = now;
-                readLatestVersion();
-            } catch (Exception x) {
-                LOG.warn("Failed to resolve latest Airsonic version.", x);
-            }
-        }
+    @Retry(name = "retryApi", fallbackMethod = "getVersionFallback")
+    public synchronized Version getLatestBetaVersion() {
+        return externalAPICaller.getLatestBetaVersion();
     }
 
-    private static final String VERSION_URL = "https://api.github.com/repos/airsonic-advanced/airsonic-advanced/releases";
-
-    private static ResponseHandler<List<Map<String, Object>>> respHandler = new AbstractResponseHandler<List<Map<String,Object>>>() {
-        @Override
-        public List<Map<String, Object>> handleEntity(HttpEntity entity) throws IOException {
-            try (InputStream is = entity.getContent(); InputStream bis = new BufferedInputStream(is)) {
-                return Util.getObjectMapper().readValue(bis, new TypeReference<List<Map<String, Object>>>() {});
-            }
-        }
-    };
-
-    private static Function<Map<String,Object>, Version> releaseToVersionMapper = r ->
-            new Version(
-                    (String) r.get("tag_name"),
-                    (String) r.get("target_commitish"),
-                    (Boolean) r.get("draft") || (Boolean) r.get("prerelease"),
-                    (String) r.get("html_url"),
-                    Instant.parse((String) r.get("published_at")),
-                    Instant.parse((String) r.get("created_at")),
-                    (List<Map<String,Object>>) r.get("assets")
-                    );
-
     /**
-     * Resolves the latest available Airsonic version by inspecting github.
+     * Returns the version number for the latest available Airsonic final version.
+     *
+     * @return The version number for the latest available Airsonic final version, or <code>null</code>
+     *         if the version number can't be resolved.
      */
-    private void readLatestVersion() throws IOException {
-
-        LOG.debug("Starting to read latest version");
-        RequestConfig requestConfig = RequestConfig.custom()
-                .setConnectTimeout(10000)
-                .setSocketTimeout(10000)
-                .build();
-        HttpGet method = new HttpGet(VERSION_URL);
-        method.setConfig(requestConfig);
-        method.setHeader("accept", "application/vnd.github.v3+json");
-        method.setHeader("User-Agent", "Airsonic/" + getLocalVersion());
-        List<Map<String, Object>> content;
-        try (CloseableHttpClient client = HttpClients.createDefault()) {
-            content = client.execute(method, respHandler);
-        } catch (ConnectTimeoutException e) {
-            LOG.warn("Got a timeout when trying to reach {}", VERSION_URL);
-            return;
-        }
-
-        List<Map<String, Object>> releases = content.stream()
-                .sorted(Comparator.<Map<String, Object>,Instant>comparing(r -> Instant.parse((String) r.get("published_at")), Comparator.reverseOrder()))
-                .collect(Collectors.toList());
-
-
-        Optional<Map<String, Object>> betaR = releases.stream().findFirst();
-        Optional<Map<String, Object>> finalR = releases.stream().filter(x -> !((Boolean)x.get("draft")) && !((Boolean)x.get("prerelease"))).findFirst();
-        Optional<Map<String,Object>> currentR = releases.stream().filter(x ->
-            StringUtils.equals(build.getProperty("version") + "." + build.getProperty("timestamp"), (String) x.get("tag_name")) ||
-            StringUtils.equals(build.getProperty("version"), (String) x.get("tag_name"))).findAny();
+    @Retry(name = "retryApi", fallbackMethod = "getVersionFallback")
+    public synchronized Version getLatestFinalVersion() {
+        return externalAPICaller.getLatestFinalVersion();
+    }
 
-        LOG.debug("Got {} for beta version", betaR.map(x -> x.get("tag_name")).orElse(null));
-        LOG.debug("Got {} for final version", finalR.map(x -> x.get("tag_name")).orElse(null));
+    /**
+     * Handles fallback for calls to check whether updated versions exist
+     *
+     * @param t Exception returned from the failed Resilience4j-protected call
+     *          to GitHub to look for updated version information.
+     * @return false, we will ignore new remote versions if the call fails
+     */
+    public boolean isNewVersionAvailableFallback(Throwable t) {
+        return false;
+    }
 
-        latestBetaVersion = betaR.map(releaseToVersionMapper).orElse(null);
-        latestFinalVersion = finalR.map(releaseToVersionMapper).orElse(null);
-        localVersion = currentR.map(releaseToVersionMapper).orElse(localVersion);
+    /**
+     * Handles fallback for calls to get version identifier
+     *
+     * @param t Exception return from failed call to get version
+     * @return the local version information
+     */
+    public Version getVersionFallback(Throwable t) {
+        return externalAPICaller.getLocalVersion();
     }
 }
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/spring/CacheConfiguration.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/spring/CacheConfiguration.java
deleted file mode 100644
index 03ee5fbd..00000000
--- a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/spring/CacheConfiguration.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.airsonic.player.spring;
-
-import org.airsonic.player.domain.CoverArt;
-import org.airsonic.player.domain.MediaFile;
-import org.airsonic.player.domain.Playlist;
-import org.airsonic.player.domain.User;
-import org.airsonic.player.domain.UserSettings;
-import org.airsonic.player.service.SettingsService;
-import org.ehcache.config.builders.CacheConfigurationBuilder;
-import org.ehcache.config.builders.ConfigurationBuilder;
-import org.ehcache.config.builders.ExpiryPolicyBuilder;
-import org.ehcache.config.builders.ResourcePoolsBuilder;
-import org.ehcache.config.units.EntryUnit;
-import org.ehcache.event.CacheEvent;
-import org.ehcache.event.CacheEventListener;
-import org.ehcache.event.EventType;
-import org.ehcache.impl.config.event.DefaultCacheEventListenerConfiguration;
-import org.ehcache.impl.config.persistence.DefaultPersistenceConfiguration;
-import org.ehcache.jsr107.EhcacheCachingProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.cache.annotation.EnableCaching;
-import org.springframework.context.annotation.AdviceMode;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-import javax.cache.Caching;
-import javax.cache.spi.CachingProvider;
-
-import java.time.Duration;
-import java.util.EnumSet;
-
-@Configuration
-@EnableCaching(proxyTargetClass = true, mode = AdviceMode.ASPECTJ) // AspectJ used so classes calling methods on self can benefit from the cache
-public class CacheConfiguration {
-    @Bean
-    public javax.cache.CacheManager jCacheCacheManager() {
-        CachingProvider provider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");
-        EhcacheCachingProvider ehcacheCachingProvider = (EhcacheCachingProvider) provider;
-        return ehcacheCachingProvider.getCacheManager(
-                ehcacheCachingProvider.getDefaultURI(),
-                createConfig(provider.getDefaultClassLoader()));
-    }
-
-    private org.ehcache.config.Configuration createConfig(final ClassLoader cl) {
-        ResourcePoolsBuilder pools = ResourcePoolsBuilder.newResourcePoolsBuilder()
-                .heap(1000L, EntryUnit.ENTRIES);
-        // If needed, but will need to register serializers for the objects being stored
-        // .offheap(10L, MemoryUnit.MB)
-        // .disk(20, MemoryUnit.MB, false);
-
-        DefaultCacheEventListenerConfiguration cacheLogging = new DefaultCacheEventListenerConfiguration(EnumSet.allOf(EventType.class), CacheLogger.class);
-
-        return ConfigurationBuilder.newConfigurationBuilder()
-                .withService(new DefaultPersistenceConfiguration(SettingsService.getAirsonicHome().resolve("cache").toFile()))
-                .withCache("userCache",
-                        CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, User.class, pools)
-                                .withClassLoader(cl)
-                                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofDays(2)))
-                                .withService(cacheLogging))
-                .withCache("userSettingsCache",
-                        CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, UserSettings.class, pools)
-                                .withClassLoader(cl)
-                                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofDays(2)))
-                                .withService(cacheLogging))
-                .withCache("mediaFilePathCache",
-                        CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, MediaFile.class, pools)
-                                .withClassLoader(cl)
-                                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofHours(2)))
-                                .withService(cacheLogging))
-                .withCache("mediaFileIdCache",
-                        CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, MediaFile.class, pools)
-                                .withClassLoader(cl)
-                                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofHours(2)))
-                                .withService(cacheLogging))
-                .withCache("coverArtCache",
-                        CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, CoverArt.class, pools)
-                                .withClassLoader(cl)
-                                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofDays(2)))
-                                .withService(cacheLogging))
-                .withCache("playlistCache",
-                        CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, Playlist.class, pools)
-                                .withClassLoader(cl)
-                                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofDays(10)))
-                                .withService(cacheLogging))
-                .withCache("playlistUsersCache",
-                        CacheConfigurationBuilder.newCacheConfigurationBuilder(Integer.class, Object.class, pools)
-                                .withClassLoader(cl)
-                                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofDays(10)))
-                                .withService(cacheLogging))
-                .build();
-    }
-
-    public static class CacheLogger implements CacheEventListener<Object, Object> {
-        private static final Logger LOG = LoggerFactory.getLogger(CacheLogger.class);
-
-        @Override
-        public void onEvent(CacheEvent<?, ?> cacheEvent) {
-            LOG.info("Cache Key: {} | EventType: {} | Old value: {} | New value: {}", cacheEvent.getKey(),
-                    cacheEvent.getType(), cacheEvent.getOldValue(), cacheEvent.getNewValue());
-        }
-    }
-
-}
diff --git a/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/spring/RedisConfig.java b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/spring/RedisConfig.java
new file mode 100644
index 00000000..31c4d3e0
--- /dev/null
+++ b/src/airsonic-advanced/airsonic-main/src/main/java/org/airsonic/player/spring/RedisConfig.java
@@ -0,0 +1,55 @@
+package org.airsonic.player.spring;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.airsonic.player.domain.User;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@EnableCaching
+@Configuration
+public class RedisConfig extends CachingConfigurerSupport {
+    @Autowired
+    private RedisConnectionFactory redisConnectionFactory;
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        objectMapper.registerSubtypes(User.class);
+        objectMapper.registerModule(new JavaTimeModule());
+        serializer.setObjectMapper(objectMapper);
+
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+        redisTemplate.setKeySerializer(new StringRedisSerializer());
+        redisTemplate.setValueSerializer(serializer);
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+
+    @Bean
+    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
+        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
+        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
+        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
+    }
+}
\ No newline at end of file
diff --git a/src/airsonic-advanced/airsonic-main/src/main/resources/application.properties b/src/airsonic-advanced/airsonic-main/src/main/resources/application.properties
index 50042512..4367b591 100644
--- a/src/airsonic-advanced/airsonic-main/src/main/resources/application.properties
+++ b/src/airsonic-advanced/airsonic-main/src/main/resources/application.properties
@@ -21,11 +21,6 @@ management.health.retries.enabled: true
 management.health.ldap.enabled=false
 spring.task.scheduling.pool.size=10
 
-# The maximum number of attempts (including the initial call as the first attempt)
-resilience4j.retry.configs.default.maxAttempts=5
-# A fixed wait duration between retry attempts
-resilience4j.retry.configs.default.waitDuration=10s
-
 spring.cloud.azure.keyvault.secret.property-source-enabled=true
 
 spring.cloud.azure.active-directory.enabled=true
@@ -33,3 +28,29 @@ spring.cloud.azure.active-directory.credential.client-id=${airsonic-application-
 spring.cloud.azure.active-directory.credential.client-secret=${airsonic-application-client-secret}
 spring.cloud.azure.active-directory.profile.tenant-id=${airsonic-application-tenant-id}
 spring.cloud.azure.active-directory.authorization-clients.graph.scopes=https://graph.microsoft.com/User.Read
+
+# set up azure cache for redis
+spring.redis.host=${airsonic-redis-host}
+spring.redis.password=${airsonic-redis-password}
+spring.redis.port=${airsonic-redis-port}
+spring.cache.type=redis
+spring.cache.redis.time-to-live=40000
+
+resilience4j.circuitbreaker.instances.CircuitBreakerService.failure-rate-threshold=50
+resilience4j.circuitbreaker.instances.CircuitBreakerService.minimum-number-of-calls=6
+resilience4j.circuitbreaker.instances.CircuitBreakerService.automatic-transition-from-open-to-half-open-enabled=true
+resilience4j.circuitbreaker.instances.CircuitBreakerService.wait-duration-in-open-state=15s
+resilience4j.circuitbreaker.instances.CircuitBreakerService.permitted-number-of-calls-in-half-open-state=3
+resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-size=10
+resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-type=count_based
+
+resilience4j.circuitbreaker.metrics.enabled=true
+resilience4j.circuitbreaker.metrics.legacy.enabled=true
+resilience4j.circuitbreaker.instances.CircuitBreakerService.register-health-indicator=true
+resilience4j.circuitbreaker.instances.CircuitBreakerService.event-consumer-buffer-size=10
+resilience4j.circuitbreaker.configs.CircuitBreakerService.registerHealthIndicator=true
+
+resilience4j.retry.instances.retryApi.max-attempts=3
+resilience4j.retry.instances.retryApi.wait-duration=3s
+resilience4j.retry.metrics.legacy.enabled=true
+resilience4j.retry.metrics.enabled=true
\ No newline at end of file
diff --git a/src/airsonic-advanced/airsonic-main/src/main/resources/application.yml b/src/airsonic-advanced/airsonic-main/src/main/resources/application.yml
deleted file mode 100644
index 648c39f6..00000000
--- a/src/airsonic-advanced/airsonic-main/src/main/resources/application.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-resilience4j.retry:
-    instances:
-        media:
-            maxRetryAttempts: 8
-            waitDuration: 10s
-            enableExponentialBackoff: true
-            exponentialBackoffMultiplier: 2
-            retryExceptions:
-                - java.nio.file.FileSystemException
-                - java.io.IOException
diff --git a/src/airsonic-advanced/airsonic-main/src/test/java/org/airsonic/player/service/SonosServiceTest.java b/src/airsonic-advanced/airsonic-main/src/test/java/org/airsonic/player/service/SonosServiceTest.java
index 00b65793..bbd91ed3 100644
--- a/src/airsonic-advanced/airsonic-main/src/test/java/org/airsonic/player/service/SonosServiceTest.java
+++ b/src/airsonic-advanced/airsonic-main/src/test/java/org/airsonic/player/service/SonosServiceTest.java
@@ -23,13 +23,4 @@
 
 public class SonosServiceTest extends TestCase {
 
-    public void testParsePlaylistIndices() {
-        SonosService sonosService = new SonosService();
-        assertEquals("[]", sonosService.parsePlaylistIndices("").toString());
-        assertEquals("[999]", sonosService.parsePlaylistIndices("999").toString());
-        assertEquals("[1, 2, 3]", sonosService.parsePlaylistIndices("1,2,3").toString());
-        assertEquals("[1, 2, 3]", sonosService.parsePlaylistIndices("2,1,3").toString());
-        assertEquals("[1, 2, 4, 5, 6, 7]", sonosService.parsePlaylistIndices("1,2,4-7").toString());
-        assertEquals("[11, 12, 15, 20, 21, 22]", sonosService.parsePlaylistIndices("11-12,15,20-22").toString());
-    }
 }
\ No newline at end of file
diff --git a/terraform/modules/app-service/main.tf b/terraform/modules/app-service/main.tf
index 17fab8d4..fcc97298 100644
--- a/terraform/modules/app-service/main.tf
+++ b/terraform/modules/app-service/main.tf
@@ -223,6 +223,8 @@ resource "azurerm_linux_web_app" "application" {
     DatabaseConfigEmbedUsername = var.database_username
     DatabaseConfigEmbedPassword = var.database_password
 
+    AIRSONIC_RETRY_DEMO = ""
+
     APPLICATIONINSIGHTS_CONNECTION_STRING = azurerm_application_insights.app_insights.connection_string
     APPLICATIONINSIGHTS_SAMPLING_REQUESTS_PER_SECOND = 10
     ApplicationInsightsAgent_EXTENSION_VERSION = "~3"
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 05899420..b39f0a09 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -12,5 +12,5 @@ variable "environment" {
 variable "location" {
   type        = string
   description = "The Azure region where all resources in this example should be created"
-  default     = "westus3"
+  default     = "eastus"
 }
