diff --git a/api/custom/version.go b/api/custom/version.go
index 85b79695726fdfff83156d82cfbc933c9dfec0c9..d5ac8e6dd77023b60f60ec84152e216375d83ee3 100644
--- a/api/custom/version.go
+++ b/api/custom/version.go
@@ -9,13 +9,14 @@ import (
 )
 
 func GetVersion(r *http.Request, rctx rcontext.RequestContext, user api.UserInfo) interface{} {
+	unstableFeatures := make(map[string]bool)
+	unstableFeatures["xyz.amorgan.blurhash"] = rctx.Config.Features.MSC2448Blurhash.Enabled
+
 	return &api.DoNotCacheResponse{
 		Payload: map[string]interface{}{
-			"Version":   version.Version,
-			"GitCommit": version.GitCommit,
-			"unstable_features": []string{
-				"xyz.amorgan.blurhash",
-			},
+			"Version":           version.Version,
+			"GitCommit":         version.GitCommit,
+			"unstable_features": unstableFeatures,
 		},
 	}
 }
diff --git a/api/r0/upload.go b/api/r0/upload.go
index 4b6692714ed557b52f6118559bf0c3f8c1813000..20518131b3bca46205640e6208de905487b49af1 100644
--- a/api/r0/upload.go
+++ b/api/r0/upload.go
@@ -58,13 +58,19 @@ func UploadMedia(r *http.Request, rctx rcontext.RequestContext, user api.UserInf
 		return api.InternalServerError("Unexpected Error")
 	}
 
-	hash, err := info_controller.GetOrCalculateBlurhash(media, rctx)
-	if err != nil {
-		rctx.Log.Warn("Failed to calculate blurhash: " + err.Error())
+	if rctx.Config.Features.MSC2448Blurhash.Enabled {
+		hash, err := info_controller.GetOrCalculateBlurhash(media, rctx)
+		if err != nil {
+			rctx.Log.Warn("Failed to calculate blurhash: " + err.Error())
+		}
+
+		return &MediaUploadedResponse{
+			ContentUri: media.MxcUri(),
+			Blurhash:   hash,
+		}
 	}
 
 	return &MediaUploadedResponse{
 		ContentUri: media.MxcUri(),
-		Blurhash:   hash,
 	}
 }
diff --git a/api/webserver/webserver.go b/api/webserver/webserver.go
index 86b5c3d3760447a2ba32acec36a69b1fd2b2f944..822381e5846ac39cf61ff4a250ce314983a9e602 100644
--- a/api/webserver/webserver.go
+++ b/api/webserver/webserver.go
@@ -135,8 +135,9 @@ func Init() *sync.WaitGroup {
 			routes["/_matrix/media/"+version+"/info/{server:[a-zA-Z0-9.:\\-_]+}/{mediaId:[a-zA-Z0-9.\\-_]+}"] = route{"GET", infoHandler}
 			routes["/_matrix/media/"+version+"/download/{server:[a-zA-Z0-9.:\\-_]+}/{mediaId:[a-zA-Z0-9.\\-_]+}"] = route{"DELETE", purgeOneHandler}
 
-			// MSC2448: Blurhash
-			routes["/_matrix/media/"+version+"/xyz.amorgan/upload"] = route{"POST", uploadHandler}
+			if config.Get().Features.MSC2448Blurhash.Enabled {
+				routes["/_matrix/media/"+version+"/xyz.amorgan/upload"] = route{"POST", uploadHandler}
+			}
 		}
 	}
 
diff --git a/common/config/conf_min_shared.go b/common/config/conf_min_shared.go
index 44ef3fcd3a292c834df28ba3b4365422aed8c5e2..d2e1ada1d0328bb32129ba21421c6a4f594d3965 100644
--- a/common/config/conf_min_shared.go
+++ b/common/config/conf_min_shared.go
@@ -7,6 +7,7 @@ type MinimumRepoConfig struct {
 	Identicons     IdenticonsConfig  `yaml:"identicons"`
 	Quarantine     QuarantineConfig  `yaml:"quarantine"`
 	TimeoutSeconds TimeoutsConfig    `yaml:"timeouts"`
+	Features       FeatureConfig     `yaml:"featureSupport"`
 }
 
 func NewDefaultMinimumRepoConfig() MinimumRepoConfig {
@@ -38,5 +39,17 @@ func NewDefaultMinimumRepoConfig() MinimumRepoConfig {
 			ClientServer: 30,
 			Federation:   120,
 		},
+		Features: FeatureConfig{
+			MSC2448Blurhash: MSC2448Config{
+				Enabled:         false,
+				MaxRenderWidth:  1024,
+				MaxRenderHeight: 1024,
+				GenerateWidth:   64,
+				GenerateHeight:  64,
+				XComponents:     4,
+				YComponents:     3,
+				Punch:           1,
+			},
+		},
 	}
 }
diff --git a/common/config/models_domain.go b/common/config/models_domain.go
index 98d60c9acecd500c43b450b9fca96c1f73970f1e..6c11d72b5349ff54147c880471a1d732e1990588 100644
--- a/common/config/models_domain.go
+++ b/common/config/models_domain.go
@@ -72,3 +72,18 @@ type TimeoutsConfig struct {
 	Federation   int `yaml:"federationTimeoutSeconds"`
 	ClientServer int `yaml:"clientServerTimeoutSeconds"`
 }
+
+type FeatureConfig struct {
+	MSC2448Blurhash MSC2448Config `yaml:"MSC2448"`
+}
+
+type MSC2448Config struct {
+	Enabled         bool `yaml:"enabled"`
+	MaxRenderWidth  int  `yaml:"maxWidth"`
+	MaxRenderHeight int  `yaml:"maxHeight"`
+	GenerateWidth   int  `yaml:"thumbWidth"`
+	GenerateHeight  int  `yaml:"thumbHeight"`
+	XComponents     int  `yaml:"xComponents"`
+	YComponents     int  `yaml:"yComponents"`
+	Punch           int  `yaml:"punch"`
+}
diff --git a/common/config/watch.go b/common/config/watch.go
index 1e64ff8c46bbeaabad7e8c8de7bf542cd015c60c..62c8fd92d1928331a125e2f0d198d8e9d9a19b83 100644
--- a/common/config/watch.go
+++ b/common/config/watch.go
@@ -60,7 +60,8 @@ func onFileChanged() {
 	bindPortChange := configNew.General.Port != configNow.General.Port
 	forwardAddressChange := configNew.General.TrustAnyForward != configNow.General.TrustAnyForward
 	forwardedHostChange := configNew.General.UseForwardedHost != configNow.General.UseForwardedHost
-	if bindAddressChange || bindPortChange || forwardAddressChange || forwardedHostChange {
+	featureChanged := hasWebFeatureChanged(configNew, configNow)
+	if bindAddressChange || bindPortChange || forwardAddressChange || forwardedHostChange || featureChanged {
 		logrus.Warn("Webserver configuration changed - remounting")
 		globals.WebReloadChan <- true
 	}
@@ -93,3 +94,11 @@ func onFileChanged() {
 	logrus.Info("Restarting recurring tasks")
 	globals.RecurringTasksReloadChan <- true
 }
+
+func hasWebFeatureChanged(configNew *MainRepoConfig, configNow *MainRepoConfig) bool {
+	if configNew.Features.MSC2448Blurhash.Enabled != configNow.Features.MSC2448Blurhash.Enabled {
+		return true
+	}
+
+	return false
+}
diff --git a/config.sample.yaml b/config.sample.yaml
index b6df78cc9a7e8af16a919664c1f5d85377118641..4b0f87f5bcc298b7fbcb7f3eb05525f517f29647 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -392,3 +392,28 @@ metrics:
 
   # The port to listen on. Cannot be the same as the general web server port.
   port: 9000
+
+# Options for controlling various MSCs/unstable features of the media repo
+# Sections of this config might disappear or be added over time. By default all
+# features are disabled in here and must be explicitly enabled to be used.
+featureSupport:
+  # MSC2248 - Blurhash
+  MSC2448:
+    # Whether or not this MSC is enabled for use in the media repo
+    enabled: false
+
+    # Maximum dimensions for converting a blurhash to an image
+    maxWidth: 1024
+    maxHeight: 1024
+
+    # Thumbnail size in pixels to use to generate the blurhash string
+    thumbWidth: 64
+    thumbHeight: 64
+
+    # The X and Y components to use. Higher numbers blur less, lower numbers blur more.
+    xComponents: 4
+    yComponents: 3
+
+    # The amount of contrast to apply when converting a blurhash to an image. Lower values
+    # make the effect more subtle, larger values make it stronger.
+    punch: 1
diff --git a/controllers/info_controller/info_controller.go b/controllers/info_controller/info_controller.go
index 223fe32549fb1754423b6fac06152d5de0f33a20..69c7eb4611f866e2d4ad3ce151ee844f81a1caa2 100644
--- a/controllers/info_controller/info_controller.go
+++ b/controllers/info_controller/info_controller.go
@@ -40,7 +40,7 @@ func GetOrCalculateBlurhash(media *types.Media, rctx rcontext.RequestContext) (s
 
 	// Resize the image to make the blurhash a bit more reasonable to calculate
 	rctx.Log.Info("Resizing image for blurhash (faster calculation)")
-	smallImg := imaging.Fill(imgSrc, 128, 128, imaging.Center, imaging.Lanczos)
+	smallImg := imaging.Fill(imgSrc, rctx.Config.Features.MSC2448Blurhash.GenerateWidth, rctx.Config.Features.MSC2448Blurhash.GenerateHeight, imaging.Center, imaging.Lanczos)
 	imgBuf := &bytes.Buffer{}
 	err = imaging.Encode(imgBuf, smallImg, imaging.PNG)
 	if err != nil {
@@ -52,7 +52,7 @@ func GetOrCalculateBlurhash(media *types.Media, rctx rcontext.RequestContext) (s
 	}
 
 	rctx.Log.Info("Calculating blurhash")
-	encoded, err := blurhash.Encode(4, 3, &decoded)
+	encoded, err := blurhash.Encode(rctx.Config.Features.MSC2448Blurhash.XComponents, rctx.Config.Features.MSC2448Blurhash.YComponents, &decoded)
 	if err != nil {
 		return "", err
 	}
diff --git a/docs/config.md b/docs/config.md
index f316436b1ff3860c266938c1221f7407fae909d7..0e98b63da0a82c61c2cc9ebda2f5f640ba98ad78 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -65,3 +65,6 @@ identicons:
 
 Per-domain configs can also be layered - just ensure that each layer has the `homeserver` property in it. They inherit
 from the main config for options not defined in their layers.
+
+Note: all feature configs which require webserver routes to be added will need to be additionally defined in the main 
+config as enabled or disabled, then turned on and off for individual domains.