diff --git a/config.sample.yaml b/config.sample.yaml
index de30b4731fa38409e3fac28ebbf217391f2a70a5..d51132e0b8be25756bf330657fdc42c1ef497c01 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -82,25 +82,29 @@ thumbnails:
   # the default for when no width or height is requested. The media repository will return
   # either an exact match or the next largest size of thumbnail.
   sizes:
-  - width: 32
-    height: 32
-  - width: 96
-    height: 96
-  - width: 320
-    height: 240
-  - width: 640
-    height: 480
-  - width: 800
-    height: 600
+    - width: 32
+      height: 32
+    - width: 96
+      height: 96
+    - width: 320
+      height: 240
+    - width: 640
+      height: 480
+    - width: 800
+      height: 600
 
   # The content types to thumbnail when requested. Types that are not supported by the media repo
   # will not be thumbnailed (adding application/json here won't work). Clients may still not request
   # thumbnails for these types - this won't make clients automatically thumbnail these file types.
   types:
-   - "image/jpeg"
-   - "image/jpg"
-   - "image/png"
-   - "image/gif"
+    - "image/jpeg"
+    - "image/jpg"
+    - "image/png"
+    - "image/gif"
+
+  # The maximum file size to thumbnail when a capable animated thumbnail is requested. If the image
+  # is larger than this, the thumbnail will be generated as a static image.
+  maxAnimateSizeBytes: 10485760 # 10MB default, 0 to disable
 
 # Controls for the rate limit functionality
 rateLimit:
diff --git a/src/github.com/turt2live/matrix-media-repo/config/config.go b/src/github.com/turt2live/matrix-media-repo/config/config.go
index e9a4aabfa1f54dfd99bf20312e415146ffac0fca..d39a0aecdb62a5c2cefd5ff974a072e644e61689 100644
--- a/src/github.com/turt2live/matrix-media-repo/config/config.go
+++ b/src/github.com/turt2live/matrix-media-repo/config/config.go
@@ -38,9 +38,10 @@ type MediaRepoConfig struct {
 	} `yaml:"downloads"`
 
 	Thumbnails struct {
-		MaxSourceBytes int64    `yaml:"maxSourceBytes"`
-		NumWorkers     int      `yaml:"numWorkers"`
-		Types          []string `yaml:"types,flow"`
+		MaxSourceBytes      int64    `yaml:"maxSourceBytes"`
+		NumWorkers          int      `yaml:"numWorkers"`
+		Types               []string `yaml:"types,flow"`
+		MaxAnimateSizeBytes int64    `yaml:"maxAnimateSizeBytes"`
 		Sizes []struct {
 			Width  int    `yaml:"width"`
 			Height int    `yaml:"height"`
diff --git a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go
index 7e6ce84b95bc8355217c696ccc7e0a42a05f4a0b..3ce7e8407961395295909644aa3601067acd61af 100644
--- a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go
+++ b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go
@@ -101,6 +101,11 @@ func (s *thumbnailService) GetThumbnail(media *types.Media, width int, height in
 		return nil, errors.New("cannot generate thumbnail for this media's content type")
 	}
 
+	if animated && config.Get().Thumbnails.MaxAnimateSizeBytes > 0 && config.Get().Thumbnails.MaxAnimateSizeBytes < media.SizeBytes {
+		s.log.Warn("Attempted to animate a media record that is too large. Assuming animated=false")
+		animated = false
+	}
+
 	forceThumbnail := false
 	if animated && !util.ArrayContains(animatedTypes, media.ContentType) {
 		s.log.Warn("Cannot animate a non-animated file. Assuming animated=false")
diff --git a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go
index 94c008b01a7a58b8706ac6116871f488ef6abe87..2f149c012b4f7fb86f8e74d3af2c77d9198bd1db 100644
--- a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go
+++ b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go
@@ -4,6 +4,11 @@ import (
 	"bytes"
 	"context"
 	"errors"
+	"fmt"
+	"image"
+	"image/draw"
+	"image/gif"
+	"os"
 
 	"github.com/disintegration/imaging"
 	"github.com/sirupsen/logrus"
@@ -69,21 +74,58 @@ func (t *thumbnailer) GenerateThumbnail(media *types.Media, width int, height in
 		}
 	}
 
-	if method == "scale" {
-		src = imaging.Fit(src, width, height, imaging.Lanczos)
-	} else if method == "crop" {
-		src = imaging.Fill(src, width, height, imaging.Center, imaging.Lanczos)
+	contentType := "image/png"
+	imgData := &bytes.Buffer{}
+	if animated && util.ArrayContains(animatedTypes, media.ContentType) {
+		t.log.Info("Generating animated thumbnail")
+		contentType = "image/gif"
+
+		// Animated GIFs are a bit more special because we need to do it frame by frame.
+		// This is fairly resource intensive. The calling code is responsible for limiting this case.
+
+		inputFile, err := os.Open(media.Location)
+		if err != nil {
+			t.log.Error("Error generating animated thumbnail: " + err.Error())
+			return nil, err
+		}
+		defer inputFile.Close()
+
+		g, err := gif.DecodeAll(inputFile)
+		if err != nil {
+			t.log.Error("Error generating animated thumbnail: " + err.Error())
+			return nil, err
+		}
+
+		for i := range g.Image {
+			frameThumb, err := thumbnailFrame(g.Image[i], method, width, height, imaging.Lanczos)
+			if err != nil {
+				t.log.Error("Error generating animated thumbnail frame: " + err.Error())
+				return nil, err
+			}
+
+			t.log.Info(fmt.Sprintf("Width = %d    Height = %d    FW=%d    FH=%d", width, height, frameThumb.Bounds().Max.X, frameThumb.Bounds().Max.Y))
+			g.Image[i] = image.NewPaletted(frameThumb.Bounds(), g.Image[i].Palette)
+			draw.Draw(g.Image[i], frameThumb.Bounds(), frameThumb, image.Pt(0, 0), draw.Over)
+		}
+
+		err = gif.EncodeAll(imgData, g)
+		if err != nil {
+			t.log.Error("Error generating animated thumbnail: " + err.Error())
+			return nil, err
+		}
 	} else {
-		t.log.Error("Unrecognized thumbnail method: " + method)
-		return nil, errors.New("unrecognized method: " + method)
-	}
+		src, err = thumbnailFrame(src, method, width, height, imaging.Lanczos)
+		if err != nil {
+			t.log.Error("Error generating thumbnail: " + err.Error())
+			return nil, err
+		}
 
-	// Put the image bytes into a memory buffer
-	imgData := &bytes.Buffer{}
-	err = imaging.Encode(imgData, src, imaging.PNG)
-	if err != nil {
-		t.log.Error("Unexpected error encoding thumbnail: " + err.Error())
-		return nil, err
+		// Put the image bytes into a memory buffer
+		err = imaging.Encode(imgData, src, imaging.PNG)
+		if err != nil {
+			t.log.Error("Unexpected error encoding thumbnail: " + err.Error())
+			return nil, err
+		}
 	}
 
 	// Reset the buffer pointer and store the file
@@ -100,8 +142,20 @@ func (t *thumbnailer) GenerateThumbnail(media *types.Media, width int, height in
 	}
 
 	thumb.DiskLocation = location
-	thumb.ContentType = "image/png"
+	thumb.ContentType = contentType
 	thumb.SizeBytes = fileSize
 
 	return thumb, nil
 }
+
+func thumbnailFrame(src image.Image, method string, width int, height int, filter imaging.ResampleFilter) (image.Image, error) {
+	if method == "scale" {
+		src = imaging.Fit(src, width, height, filter)
+	} else if method == "crop" {
+		src = imaging.Fill(src, width, height, imaging.Center, filter)
+	} else {
+		return nil, errors.New("unrecognized method: " + method)
+	}
+
+	return src, nil
+}