diff --git a/CHANGELOG.md b/CHANGELOG.md
index a36673e4485a754b6979045fa47c6b9aeb86bb05..a46257ac5d0403522fbca600097484585153d297 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 * Add webp image support. Thanks @Sorunome!
 * Add apng image support. Thanks @Sorunome!
+* Experimental support for Redis as a cache (in preparation for proper load balancing/HA support).
 
 ### Changed
 
diff --git a/cmd/media_repo/main.go b/cmd/media_repo/main.go
index ee80e9df453ea682b48487ba76781efb1f3c250b..3c6a0f48ae00ae14f60ade98d2f54c77c102b6ed 100644
--- a/cmd/media_repo/main.go
+++ b/cmd/media_repo/main.go
@@ -45,6 +45,7 @@ func main() {
 
 	logrus.Info("Starting up...")
 	runtime.RunStartupSequence()
+	internal_cache.ReplaceInstance() // init the cache as we may be using Redis, and it'd be good to get going sooner
 
 	logrus.Info("Checking background tasks...")
 	err = scanAndStartUnfinishedTasks()
@@ -74,7 +75,6 @@ func main() {
 
 		logrus.Info("Stopping recurring tasks...")
 		tasks.StopAll()
-		internal_cache.Get().Stop()
 	}
 
 	// Set up a listener for SIGINT
diff --git a/cmd/media_repo/reloads.go b/cmd/media_repo/reloads.go
index 63452d2d0e373d078c5c941b34106584199caf9a..6deb426d3927f357bf19b613904c469d5ebaeed3 100644
--- a/cmd/media_repo/reloads.go
+++ b/cmd/media_repo/reloads.go
@@ -5,6 +5,7 @@ import (
 	"github.com/turt2live/matrix-media-repo/api/webserver"
 	"github.com/turt2live/matrix-media-repo/common/globals"
 	"github.com/turt2live/matrix-media-repo/common/runtime"
+	"github.com/turt2live/matrix-media-repo/internal_cache"
 	"github.com/turt2live/matrix-media-repo/ipfs_proxy"
 	"github.com/turt2live/matrix-media-repo/metrics"
 	"github.com/turt2live/matrix-media-repo/storage"
@@ -19,6 +20,7 @@ func setupReloads() {
 	reloadRecurringTasksOnChan(globals.RecurringTasksReloadChan)
 	reloadIpfsOnChan(globals.IPFSReloadChan)
 	reloadAccessTokensOnChan(globals.AccessTokenReloadChan)
+	reloadCacheOnChan(globals.CacheReplaceChan)
 }
 
 func stopReloads() {
@@ -30,6 +32,7 @@ func stopReloads() {
 	globals.AccessTokenReloadChan <- false
 	globals.RecurringTasksReloadChan <- false
 	globals.IPFSReloadChan <- false
+	globals.CacheReplaceChan <- false
 }
 
 func reloadWebOnChan(reloadChan chan bool) {
@@ -130,3 +133,17 @@ func reloadAccessTokensOnChan(reloadChan chan bool) {
 		}
 	}()
 }
+
+func reloadCacheOnChan(reloadChan chan bool) {
+	go func() {
+		defer close(reloadChan)
+		for {
+			shouldReload := <-reloadChan
+			if shouldReload {
+				internal_cache.ReplaceInstance()
+			} else {
+				internal_cache.Get().Stop()
+			}
+		}
+	}()
+}
diff --git a/common/config/conf_min_shared.go b/common/config/conf_min_shared.go
index 8e05b88f9c2bda0dfe1a6c385bde6a1d57813175..4059b92866ebeb9ce58e07f68cbd868987a77c72 100644
--- a/common/config/conf_min_shared.go
+++ b/common/config/conf_min_shared.go
@@ -56,6 +56,10 @@ func NewDefaultMinimumRepoConfig() MinimumRepoConfig {
 					RepoPath: "./ipfs",
 				},
 			},
+			Redis: RedisConfig{
+				Enabled: false,
+				Shards: []RedisShardConfig{},
+			},
 		},
 		AccessTokens: AccessTokenConfig{
 			MaxCacheTimeSeconds: 0,
diff --git a/common/config/models_domain.go b/common/config/models_domain.go
index 5a14158af050b444a9f35d80fe3c339c7426a233..db3ec77fb99dd1337526e75b998887f4ff631dd5 100644
--- a/common/config/models_domain.go
+++ b/common/config/models_domain.go
@@ -7,9 +7,9 @@ type ArchivingConfig struct {
 }
 
 type UploadsConfig struct {
-	MaxSizeBytes         int64               `yaml:"maxBytes"`
-	MinSizeBytes         int64               `yaml:"minBytes"`
-	ReportedMaxSizeBytes int64               `yaml:"reportedMaxBytes"`
+	MaxSizeBytes         int64 `yaml:"maxBytes"`
+	MinSizeBytes         int64 `yaml:"minBytes"`
+	ReportedMaxSizeBytes int64 `yaml:"reportedMaxBytes"`
 }
 
 type DatastoreConfig struct {
@@ -74,6 +74,7 @@ type TimeoutsConfig struct {
 type FeatureConfig struct {
 	MSC2448Blurhash MSC2448Config `yaml:"MSC2448"`
 	IPFS            IPFSConfig    `yaml:"IPFS"`
+	Redis           RedisConfig   `yaml:"redis"`
 }
 
 type MSC2448Config struct {
@@ -97,6 +98,16 @@ type IPFSDaemonConfig struct {
 	RepoPath string `yaml:"repoPath"`
 }
 
+type RedisConfig struct {
+	Enabled bool               `yaml:"enabled"`
+	Shards  []RedisShardConfig `yaml:"shards,flow"`
+}
+
+type RedisShardConfig struct {
+	Name    string `yaml:"name"`
+	Address string `yaml:"addr"`
+}
+
 type AccessTokenConfig struct {
 	MaxCacheTimeSeconds int                `yaml:"maxCacheTimeSeconds"`
 	UseAppservices      bool               `yaml:"useLocalAppserviceConfig"`
diff --git a/common/config/watch.go b/common/config/watch.go
index c7f02d03b5e62dd6364c4be33157404b5e963322..5dede9f0eb3610500248e7ca17dadbc75be5a236 100644
--- a/common/config/watch.go
+++ b/common/config/watch.go
@@ -95,6 +95,20 @@ func onFileChanged() {
 		globals.IPFSReloadChan <- true
 	}
 
+	redisEnabledChange := configNew.Features.Redis.Enabled != configNow.Features.Redis.Enabled
+	redisShardsChange := hasRedisShardConfigChanged(configNew, configNow)
+	cacheEnabledChange := configNew.Downloads.Cache.Enabled != configNow.Downloads.Cache.Enabled
+	cacheMaxSizeChange := configNew.Downloads.Cache.MaxSizeBytes != configNow.Downloads.Cache.MaxSizeBytes
+	cacheMaxFileSizeChange := configNew.Downloads.Cache.MaxFileSizeBytes != configNow.Downloads.Cache.MaxFileSizeBytes
+	cacheTrackedMinChange := configNew.Downloads.Cache.TrackedMinutes != configNow.Downloads.Cache.TrackedMinutes
+	cacheMinDownloadsChange := configNew.Downloads.Cache.MinDownloads != configNow.Downloads.Cache.MinDownloads
+	cacheMinCacheTimeChange := configNew.Downloads.Cache.MinCacheTimeSeconds != configNow.Downloads.Cache.MinCacheTimeSeconds
+	cacheMinEvictedTimeChange := configNew.Downloads.Cache.MinEvictedTimeSeconds != configNow.Downloads.Cache.MinEvictedTimeSeconds
+	if redisEnabledChange || redisShardsChange || cacheEnabledChange || cacheMaxSizeChange || cacheMaxFileSizeChange || cacheTrackedMinChange || cacheMinDownloadsChange || cacheMinCacheTimeChange || cacheMinEvictedTimeChange {
+		logrus.Warn("Cache configuration changed - reloading")
+		globals.CacheReplaceChan <- true
+	}
+
 	// Always flush the access token cache
 	logrus.Warn("Flushing access token cache")
 	globals.AccessTokenReloadChan <- true
@@ -117,3 +131,26 @@ func hasWebFeatureChanged(configNew *MainRepoConfig, configNow *MainRepoConfig)
 
 	return false
 }
+
+func hasRedisShardConfigChanged(configNew *MainRepoConfig, configNow *MainRepoConfig) bool {
+	oldShards := configNow.Features.Redis.Shards
+	newShards := configNew.Features.Redis.Shards
+	if len(oldShards) != len(newShards) {
+		return true
+	}
+
+	for _, s1 := range oldShards {
+		has := false
+		for _, s2 := range newShards {
+			if s1.Name == s2.Name && s1.Address == s2.Address {
+				has = true
+				break
+			}
+		}
+		if !has {
+			return true
+		}
+	}
+
+	return false
+}
\ No newline at end of file
diff --git a/common/globals/reload.go b/common/globals/reload.go
index b488aec1040082b6d04716782a1139384b674269..f1aa971fff146621cdaaf631aa2fedd8a0d10c34 100644
--- a/common/globals/reload.go
+++ b/common/globals/reload.go
@@ -6,4 +6,5 @@ var DatabaseReloadChan = make(chan bool)
 var DatastoresReloadChan = make(chan bool)
 var RecurringTasksReloadChan = make(chan bool)
 var IPFSReloadChan = make(chan bool)
-var AccessTokenReloadChan = make(chan bool)
\ No newline at end of file
+var AccessTokenReloadChan = make(chan bool)
+var CacheReplaceChan = make(chan bool)
\ No newline at end of file
diff --git a/config.sample.yaml b/config.sample.yaml
index 99867d39e86b706005c8c1ba17cdc6334138cad9..0e99a7236087b95e7a2ddfb725c5ec003f390b5d 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -485,4 +485,24 @@ featureSupport:
       # If the Daemon is enabled, set this to the location where the IPFS files should
       # be stored. If you're using Docker, this should be something like "/data/ipfs"
       # so it can be mapped to a volume.
-      repoPath: "./ipfs"
\ No newline at end of file
+      repoPath: "./ipfs"
+
+  # Support for redis as a cache mechanism
+  #
+  # Note: Enabling Redis support will mean that the existing cache mechanism will do nothing.
+  # It can be safely disabled once Redis support is enabled.
+  #
+  # See docs/redis.md for more information on how this works and how to set it up.
+  redis:
+    # Whether or not use Redis instead of in-process caching.
+    enabled: false
+
+    # The Redis shards that should be used by the media repo in the ring. The names of the
+    # shards are for your reference and have no bearing on the connection, but must be unique.
+    shards:
+      - name: "server1"
+        addr: ":7000"
+      - name: "server2"
+        addr: ":7001"
+      - name: "server3"
+        addr: ":7002"
\ No newline at end of file
diff --git a/docs/config.md b/docs/config.md
index 25160c8302d3b79276530c43e9dbd687fd77ae06..831ac5af964116e7f70910b2eee4bf020d892fdc 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -55,6 +55,7 @@ Any options from the main config can then be overridden per-domain with the exce
 * `thumbnails.expireAfterDays` - because thumbnails aren't associated with any particular domain.
 * `urlPreviews.expireAfterDays` - because previews aren't associated with any particular domain.
 * `featureSupport.IPFS.builtInDaemon` - because spawning multiple daemons doesn't make sense.
+* `featureSupport.redis` - because the cache is repo-wide.
 
 To override a value, simply provide it in any valid per-domain config:
 
diff --git a/docs/redis.md b/docs/redis.md
new file mode 100644
index 0000000000000000000000000000000000000000..0cee83af324b18f0dfc150de2e1be64442821416
--- /dev/null
+++ b/docs/redis.md
@@ -0,0 +1,49 @@
+# Redis support
+
+**Note**: Redis support is currently experimental and not intended for usage in general cases.
+
+Redis can be used as a high-performance cache for the media repo, allowing for (in future) multiple media
+repositories to run concurrently in limited jobs (such as some processes handling uploads, others downloads,
+etc). Currently though, it is capable of speeding up deployments of disjointed media repos or in preparation
+for proper load balancer support by the media repo.
+
+The media repo connects to a number of "shards" (Redis processes) to distribute cached keys over them. Each
+shard is not expected to store persistent data and should be tolerant of total failure - the media repo assumes
+that the shards will be dedicated to caching and thus will not have any expectations that a particular shard
+will remain running.
+
+Setting up a shard is fairly simple: it's the same as deploying Redis itself. The media repo does not manage
+the expiration policy for the shards, so it is recommended to give https://redis.io/topics/lru-cache a read to
+pick the best eviction policy for your environment. The current recommendations are:
+* A `maxmemory` of at least `1gb` for each shard.
+* A `maxmemory-policy` of `allkeys-lfu` to ensure that the cache gets cleared out (the media repo does not set
+  an expiration time or TTL). Note: an `lru` mode is *not* recommended as the media repo will be caching all
+  uploads it sees, which includes remote media. A `lfu` mode ensures that recent items being cached can still
+  be evicted if not commonly requested.
+* 1 shard for most deployments. Larger repos (or connecting many media repos to the same shards) should consider
+  3 or more shards.
+
+The shards in the ring can be changed at runtime by updating the config and ensuring the media repo has reloaded
+the config. Note that changing cache mechanisms at runtime is not recommended, and a full restart is recommended
+instead.
+
+**Note**: Metrics reported for cache size will be inaccurate. Frequencies of requests will still be reported.
+
+**Note**: Quarantined media will still be stored in the cache. This is considered a bug and will need fixing.
+
+## Connecting multiple disjointed media repos
+
+Though the media repo expects to be the sole and only thing in the datacenter handling media, it is not always
+possible or sane to do so. Examples including hosting providers which may have several media repos handling a
+small subset of domains each. In these scenarios, it may be beneficial to set up a series of Redis shards within
+each datacenter and connect all the media repos in that DC to them. This can reduce the amount of time it takes
+to retrieve media from a media repo in that DC, as well as avoid downloading several copies of remote media.
+
+Note that even when connecting media repos to the same set of shards the repos will still attempt to upload a
+copy of the media to the datastore. For example, if media repo A downloads something from matrix.org and puts
+it into the cache, media repo B will first get it from the cache and upload it to its datastore when a user 
+requests the same media. The benefit, however, is that only 1 request to matrix.org happened instead of two.
+
+All media repos should be connected to the same set of shards to ensure even balancing between the shards.
+Additionally, all media repos **must** be running the same major version (anything in `1.x.x`) in order to avoid 
+conflicts.
diff --git a/go.mod b/go.mod
index 7ba66ab05a129bb7543a1f8b09a82e92db15dab8..8c4ddf00cb93f58f0637aa594efb2787bc16efe2 100644
--- a/go.mod
+++ b/go.mod
@@ -22,8 +22,10 @@ require (
 	github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
 	github.com/fogleman/gg v1.3.0
 	github.com/fsnotify/fsnotify v1.4.7
+	github.com/go-redis/redis/v8 v8.0.0-beta.6
 	github.com/go-sql-driver/mysql v1.5.0 // indirect
 	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
+	github.com/golang/snappy v0.0.1 // indirect
 	github.com/gorilla/mux v1.7.4
 	github.com/h2non/filetype v1.0.12
 	github.com/ipfs/go-cid v0.0.4
@@ -56,7 +58,7 @@ require (
 	github.com/tebeka/strftime v0.1.3 // indirect
 	golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
 	golang.org/x/image v0.0.0-20200119044424-58c23975cae1
-	golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
+	golang.org/x/net v0.0.0-20200513185701-a91f0712d120 // indirect
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 	gopkg.in/ini.v1 v1.52.0 // indirect
 	gopkg.in/yaml.v2 v2.2.8
diff --git a/go.sum b/go.sum
index ce5d5e0deba2c81041784c60672fc962f1dad98a..330d93e64df0542ce5b2a151b21ff4ae8a82822b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,16 +1,22 @@
 bazil.org/fuse v0.0.0-20180421153158-65cc252bf669 h1:FNCRpXiquG1aoyqcIWVFmpTSKVcx2bQD38uZZeGtdlw=
 bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/AndreasBriese/bbloom v0.0.0-20190823232136-616930265c33 h1:2/E2IVdZoHh/aCBq4Gchy2MGWkTmbReP46/Wnt9qhKs=
 github.com/AndreasBriese/bbloom v0.0.0-20190823232136-616930265c33/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs=
+github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
 github.com/DavidHuie/gomigrate v0.0.0-20190826182718-4adc4b3de142 h1:pfeJevnIXt4KJShhkTp8uRU0evDkRaFkAdmaNmzHMIQ=
 github.com/DavidHuie/gomigrate v0.0.0-20190826182718-4adc4b3de142/go.mod h1:F3GZLX+VN44AjFiyKD8++nq8sVE0Sw3bOhhQ3mUffnM=
 github.com/Jeffail/tunny v0.0.0-20190930221602-f13eb662a36a h1:sk14oPN106XTe3WzOIaVGq+cFh1sh4z++2pAg2j4XCo=
 github.com/Jeffail/tunny v0.0.0-20190930221602-f13eb662a36a/go.mod h1:BX3q3G70XX0UmIkDWfDHoDRquDS1xFJA5VTbMf+14wM=
 github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y=
+github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
 github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
 github.com/Stebalien/go-bitfield v0.0.0-20180330043415-076a62f9ce6e/go.mod h1:3oM7gXIttpYDAJXpVNnSCiUMYBLIZ6cb1t+Ip982MRo=
@@ -30,6 +36,8 @@ github.com/alioygur/is v1.0.3/go.mod h1:fmXi78K26iMaOs0fINRVLl1TIPCYcLfOopoZ5+mc
 github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
 github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg=
+github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -58,6 +66,9 @@ github.com/cenk/backoff v2.2.1+incompatible h1:djdFT7f4gF2ttuzRKPbMOWgZajgesItGL
 github.com/cenk/backoff v2.2.1+incompatible/go.mod h1:7FtoeaSnHoZnmZzz47cM35Y9nSW7tNyaidugnHTaFDE=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ=
@@ -65,6 +76,7 @@ github.com/chai2010/webp v1.1.0/go.mod h1:LP12PG5IFmLGHUU26tBiCBKnghxx3toZFwDjOY
 github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
 github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -89,6 +101,8 @@ github.com/dgraph-io/badger v1.6.0-rc1/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhY
 github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-rendezvous v0.0.0-20200624174652-8d2f3be8b2d9 h1:h2Ul3Ym2iVZWMQGYmulVUJ4LSkBm1erp9mUkPwtMoLg=
+github.com/dgryski/go-rendezvous v0.0.0-20200624174652-8d2f3be8b2d9/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
 github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
 github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
@@ -100,6 +114,9 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
 github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8 h1:6muCmMJat6z7qptVrIf/+OWPxsjAfvhw5/6t+FwEkgg=
 github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8/go.mod h1:nYia/MIs9OyvXXYboPmNOj0gVWo97Wx0sde+ZuKkoM4=
 github.com/elgris/jsondiff v0.0.0-20160530203242-765b5c24c302/go.mod h1:qBlWZqWeVx9BjvqBsnC/8RUlAYpIFmPvgROcw0n1scE=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 h1:BBso6MBKW8ncyZLv37o+KNyy0HrrHgfnOaGQC2qvN+A=
 github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5/go.mod h1:JpoxHjuQauoxiFMl1ie8Xc/7TfLuMZ5eOCONd1sUBHg=
 github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
@@ -115,10 +132,13 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
 github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
 github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-redis/redis/v8 v8.0.0-beta.6 h1:QeXAkG9L5cWJA+eJTBvhkftE7dwpJ0gbMYeBE2NxXS4=
+github.com/go-redis/redis/v8 v8.0.0-beta.6/go.mod h1:g79Vpae8JMzg5qjk8BiwU9tK+HmU3iDVyS4UAJLFycI=
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -141,13 +161,26 @@ github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -687,12 +720,18 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e h1:fI6mGTyggeIYVmGhf80XFHxTupjOexbCppgTNDkv9AA=
+github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -715,6 +754,7 @@ github.com/prometheus/client_golang v1.4.1 h1:FFSuS004yOQEtDdTq+TAOLP5xUq63KqAFY
 github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
@@ -774,6 +814,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto=
@@ -820,6 +862,8 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50=
 go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
+go.opentelemetry.io/otel v0.7.0 h1:u43jukpwqR8EsyeJOMgrsUgZwVI1e1eVw7yuzRkD1l0=
+go.opentelemetry.io/otel v0.7.0/go.mod h1:aZMyHG5TqDOXEgH2tyLiXSUKly1jT3yqE9PmrzIeCdo=
 go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
@@ -853,12 +897,20 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20200513190911-00229845015e h1:rMqLP+9XLy+LdbCXHjJHAmTfXCr93W7oruWA6Hq1Alc=
+golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
 golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -876,10 +928,11 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -898,6 +951,7 @@ golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -910,6 +964,8 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190926180325-855e68c8590b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
@@ -928,7 +984,10 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -937,8 +996,22 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
 google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8=
+google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -958,6 +1031,10 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal_cache/media_cache.go b/internal_cache/media_cache.go
index ba4ca11d291950ce1ac6e50710a9ecaa9f20379c..69edee6fa31fe6e26302136e24d826f2f51a1aad 100644
--- a/internal_cache/media_cache.go
+++ b/internal_cache/media_cache.go
@@ -15,6 +15,7 @@ import (
 	"github.com/turt2live/matrix-media-repo/common/config"
 	"github.com/turt2live/matrix-media-repo/common/rcontext"
 	"github.com/turt2live/matrix-media-repo/metrics"
+	"github.com/turt2live/matrix-media-repo/redis_cache"
 	"github.com/turt2live/matrix-media-repo/storage/datastore"
 	"github.com/turt2live/matrix-media-repo/types"
 	"github.com/turt2live/matrix-media-repo/util"
@@ -29,6 +30,7 @@ type MediaCache struct {
 	size          int64
 	enabled       bool
 	cleanupTimer  *time.Ticker
+	redis *redis_cache.RedisCache
 }
 
 type cachedFile struct {
@@ -52,7 +54,10 @@ func Get() *MediaCache {
 	}
 
 	lock.Do(func() {
-		if !config.Get().Downloads.Cache.Enabled {
+		if config.Get().Features.Redis.Enabled {
+			logrus.Info("Setting up Redis cache")
+			instance = &MediaCache{enabled: true, redis: redis_cache.NewCache()}
+		} else if !config.Get().Downloads.Cache.Enabled {
 			logrus.Warn("Cache is disabled - setting up a dummy instance")
 			instance = &MediaCache{enabled: false}
 		} else {
@@ -93,11 +98,27 @@ func Get() *MediaCache {
 	return instance
 }
 
+func ReplaceInstance() {
+	if instance != nil {
+		instance.Reset()
+		instance.Stop()
+		instance = nil
+	}
+
+	// call Get() to update the instance reference
+	Get()
+}
+
 func (c *MediaCache) Reset() {
 	if !c.enabled {
 		return
 	}
 
+	if c.redis != nil {
+		logrus.Warn("Not doing a cache reset - using Redis cache")
+		return
+	}
+
 	logrus.Warn("Resetting media cache")
 	rwLock.Lock()
 	c.cache.Flush()
@@ -108,10 +129,19 @@ func (c *MediaCache) Reset() {
 }
 
 func (c *MediaCache) Stop() {
-	c.cleanupTimer.Stop()
+	if c.redis != nil {
+		_ = c.redis.Close()
+	} else {
+		c.cleanupTimer.Stop()
+	}
 }
 
 func (c *MediaCache) getUnderlyingUsedBytes() int64 {
+	if c.redis != nil {
+		// Cannot determine: return implied result
+		return 0
+	}
+
 	var size int64 = 0
 	for _, entry := range c.cache.Items() {
 		f := entry.Object.(*cachedFile)
@@ -121,6 +151,11 @@ func (c *MediaCache) getUnderlyingUsedBytes() int64 {
 }
 
 func (c *MediaCache) getUnderlyingItemCount() int {
+	if c.redis != nil {
+		// Cannot determine: return implied result
+		return 0
+	}
+
 	return c.cache.ItemCount()
 }
 
@@ -128,6 +163,10 @@ func (c *MediaCache) IncrementDownloads(fileHash string) {
 	if !c.enabled {
 		return
 	}
+	if c.redis != nil {
+		// Irrelevant data point
+		return
+	}
 
 	logrus.Info("File " + fileHash + " has been downloaded")
 	rwLock.Lock()
@@ -155,7 +194,8 @@ func (c *MediaCache) GetMedia(media *types.Media, ctx rcontext.RequestContext) (
 		return &cachedFile{media: media, Contents: bytes.NewBuffer(data)}, nil
 	}
 
-	return c.updateItemInCache(media.Sha256Hash, media.SizeBytes, cacheFn, ctx)
+	tmpl := &cachedFile{media: media}
+	return c.updateItemInCache(media.Sha256Hash, media.SizeBytes, cacheFn, tmpl, ctx)
 }
 
 func (c *MediaCache) GetThumbnail(thumbnail *types.Thumbnail, ctx rcontext.RequestContext) (*cachedFile, error) {
@@ -178,10 +218,34 @@ func (c *MediaCache) GetThumbnail(thumbnail *types.Thumbnail, ctx rcontext.Reque
 		return &cachedFile{thumbnail: thumbnail, Contents: bytes.NewBuffer(data)}, nil
 	}
 
-	return c.updateItemInCache(thumbnail.Sha256Hash, thumbnail.SizeBytes, cacheFn, ctx)
+	tmpl := &cachedFile{thumbnail: thumbnail}
+	return c.updateItemInCache(thumbnail.Sha256Hash, thumbnail.SizeBytes, cacheFn, tmpl, ctx)
 }
 
-func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn func() (*cachedFile, error), ctx rcontext.RequestContext) (*cachedFile, error) {
+func (c *MediaCache) updateItemInCache(recordId string, mediaSize int64, cacheFn func() (*cachedFile, error), template *cachedFile, ctx rcontext.RequestContext) (*cachedFile, error) {
+	if c.redis != nil {
+		b, err := c.redis.GetBytes(ctx, recordId)
+		if err == redis_cache.ErrCacheMiss || err == redis_cache.ErrCacheDown {
+			metrics.CacheMisses.With(prometheus.Labels{"cache": "media"}).Inc()
+			cf, err := cacheFn()
+			if err != nil {
+				return nil, err
+			}
+			b, err := ioutil.ReadAll(cf.Contents)
+			err = c.redis.SetStream(ctx, recordId, bytes.NewBuffer(b))
+			if err != nil && err != redis_cache.ErrCacheDown {
+				return nil, err
+			}
+			cf.Contents = bytes.NewBuffer(b)
+			return cf, nil
+		} else if err != nil {
+			return nil, err
+		}
+		metrics.CacheNumItems.With(prometheus.Labels{"cache": "media"}).Inc()
+		template.Contents = bytes.NewBuffer(b)
+		return template, nil
+	}
+
 	downloads := c.tracker.NumDownloads(recordId)
 	enoughDownloads := downloads >= config.Get().Downloads.Cache.MinDownloads
 	canCache := c.canJoinCache(recordId)
diff --git a/redis_cache/redis.go b/redis_cache/redis.go
new file mode 100644
index 0000000000000000000000000000000000000000..76b04c7818e39d901bd4983622572b22e2a3862f
--- /dev/null
+++ b/redis_cache/redis.go
@@ -0,0 +1,98 @@
+package redis_cache
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"io/ioutil"
+	"time"
+
+	"github.com/go-redis/redis/v8"
+	"github.com/sirupsen/logrus"
+	"github.com/turt2live/matrix-media-repo/common/config"
+	"github.com/turt2live/matrix-media-repo/common/rcontext"
+)
+
+var ErrCacheMiss = errors.New("missed cache")
+var ErrCacheDown = errors.New("all shards appear to be down")
+
+type RedisCache struct {
+	ring *redis.Ring
+}
+
+func NewCache() *RedisCache {
+	addresses := make(map[string]string)
+	for _, c := range config.Get().Features.Redis.Shards {
+		addresses[c.Name] = c.Address
+	}
+	ring := redis.NewRing(&redis.RingOptions{
+		Addrs: addresses,
+		DialTimeout: 10 * time.Second,
+	})
+
+	logrus.Info("Contacting Redis shards...")
+	_ = ring.ForEachShard(context.Background(), func(ctx context.Context, client *redis.Client) error {
+		logrus.Infof("Pinging %s", client.String())
+		r, err := client.Ping(ctx).Result()
+		if err != nil {
+			return err
+		}
+		logrus.Infof("%s replied with: %s", client.String(), r)
+		return nil
+	})
+
+	return &RedisCache{ring: ring}
+}
+
+func (c *RedisCache) Close() error {
+	return c.ring.Close()
+}
+
+func (c *RedisCache) SetStream(ctx rcontext.RequestContext, key string, s io.Reader) error {
+	b, err := ioutil.ReadAll(s)
+	if err != nil {
+		return err
+	}
+	return c.SetBytes(ctx, key, b)
+}
+
+func (c *RedisCache) GetStream(ctx rcontext.RequestContext, key string) (io.Reader, error) {
+	b, err := c.GetBytes(ctx, key)
+	if err != nil {
+		return nil, err
+	}
+	return bytes.NewBuffer(b), nil
+}
+
+func (c *RedisCache) SetBytes(ctx rcontext.RequestContext, key string, b []byte) error {
+	if c.ring.PoolStats().TotalConns == 0 {
+		return ErrCacheDown
+	}
+	_, err := c.ring.Set(ctx.Context, key, b, time.Duration(0)).Result() // no expiration (zero)
+	if err != nil && c.ring.PoolStats().TotalConns == 0 {
+		ctx.Log.Error(err)
+		return ErrCacheDown
+	}
+	return err
+}
+
+func (c *RedisCache) GetBytes(ctx rcontext.RequestContext, key string) ([]byte, error) {
+	if c.ring.PoolStats().TotalConns == 0 {
+		return nil, ErrCacheDown
+	}
+	r := c.ring.Get(ctx.Context, key)
+	if r.Err() != nil {
+		if r.Err() == redis.Nil {
+			return nil, ErrCacheMiss
+		}
+		if c.ring.PoolStats().TotalConns == 0 {
+			ctx.Log.Error(r.Err())
+			return nil, ErrCacheDown
+		}
+		return nil, r.Err()
+	}
+
+	b, err := r.Bytes()
+	return b, err
+}