diff --git a/changelogs/unreleased/authorize-upload-filesize.yml b/changelogs/unreleased/authorize-upload-filesize.yml
new file mode 100644
index 0000000000000000000000000000000000000000..24bcfce458e76eb43e33cf940b889c17c68b7b0f
--- /dev/null
+++ b/changelogs/unreleased/authorize-upload-filesize.yml
@@ -0,0 +1,5 @@
+---
+title: Reject upload when filesize exceeds MaximumSize returned by authorize endpoint
+merge_request:
+author:
+type: added
diff --git a/internal/api/api.go b/internal/api/api.go
index 9fa45267d84133ea2ad49ff96506a0b8a63fc975..5db947a157f82aa90a8215081ec088370219e632 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -148,6 +148,8 @@ type Response struct {
 	ProcessLsif bool
 	// Detects whether LSIF artifact will be parsed with references
 	ProcessLsifReferences bool
+	// The maximum accepted size in bytes of the upload
+	MaximumSize int64
 }
 
 // singleJoiningSlash is taken from reverseproxy.go:NewSingleHostReverseProxy
diff --git a/internal/artifacts/artifacts_store_test.go b/internal/artifacts/artifacts_store_test.go
index 2da3b7535654e30b60044d19fc3de795fc90969b..784acb1428a9e028480508eda8e335bd5c125dad 100644
--- a/internal/artifacts/artifacts_store_test.go
+++ b/internal/artifacts/artifacts_store_test.go
@@ -260,13 +260,12 @@ func TestUploadHandlerMultipartUploadSizeLimit(t *testing.T) {
 
 	objectURL := server.URL + test.ObjectPath
 
-	partSize := int64(1)
 	uploadSize := 10
 	preauth := api.Response{
 		RemoteObject: api.RemoteObject{
 			ID: "store-id",
 			MultipartUpload: &api.MultipartUploadParams{
-				PartSize:    partSize,
+				PartSize:    1,
 				PartURLs:    []string{objectURL + "?partNumber=1"},
 				AbortURL:    objectURL, // DELETE
 				CompleteURL: objectURL, // POST
@@ -292,3 +291,47 @@ func TestUploadHandlerMultipartUploadSizeLimit(t *testing.T) {
 	assert.False(t, os.IsMultipartUpload(test.ObjectPath), "MultipartUpload should not be in progress anymore")
 	assert.Empty(t, os.GetObjectMD5(test.ObjectPath), "upload should have failed, so the object should not exists")
 }
+
+func TestUploadHandlerMultipartUploadMaximumSizeFromApi(t *testing.T) {
+	os, server := test.StartObjectStore()
+	defer server.Close()
+
+	err := os.InitiateMultipartUpload(test.ObjectPath)
+	require.NoError(t, err)
+
+	objectURL := server.URL + test.ObjectPath
+
+	uploadSize := int64(10)
+	maxSize := uploadSize - 1
+	preauth := api.Response{
+		MaximumSize: maxSize,
+		RemoteObject: api.RemoteObject{
+			ID: "store-id",
+			MultipartUpload: &api.MultipartUploadParams{
+				PartSize:    uploadSize,
+				PartURLs:    []string{objectURL + "?partNumber=1"},
+				AbortURL:    objectURL, // DELETE
+				CompleteURL: objectURL, // POST
+			},
+		},
+	}
+
+	responseProcessor := func(w http.ResponseWriter, r *http.Request) {
+		t.Fatal("it should not be called")
+	}
+
+	ts := testArtifactsUploadServer(t, preauth, responseProcessor)
+	defer ts.Close()
+
+	contentBuffer, contentType := createTestMultipartForm(t, make([]byte, uploadSize))
+	response := testUploadArtifacts(t, contentType, ts.URL+Path, &contentBuffer)
+	require.Equal(t, http.StatusRequestEntityTooLarge, response.Code)
+
+	testhelper.Retry(t, 5*time.Second, func() error {
+		if os.GetObjectMD5(test.ObjectPath) == "" {
+			return nil
+		}
+
+		return fmt.Errorf("file is still present")
+	})
+}
diff --git a/internal/filestore/file_handler.go b/internal/filestore/file_handler.go
index 581ccda168e12245f3cb3036a02fe7c7e55b691b..3f345642131e8a74022691d8091e0077f025045f 100644
--- a/internal/filestore/file_handler.go
+++ b/internal/filestore/file_handler.go
@@ -166,7 +166,14 @@ func SaveFileFromReader(ctx context.Context, reader io.Reader, size int64, opts
 		if err != nil {
 			return nil, err
 		}
+
 		writers = append(writers, remoteWriter)
+
+		defer func() {
+			if err != nil {
+				remoteWriter.CloseWithError(err)
+			}
+		}()
 	} else {
 		clientMode = "local"
 
@@ -182,12 +189,26 @@ func SaveFileFromReader(ctx context.Context, reader io.Reader, size int64, opts
 		return nil, errors.New("missing upload destination")
 	}
 
+	if opts.MaximumSize > 0 {
+		if size > opts.MaximumSize {
+			return nil, SizeError(fmt.Errorf("the upload size %d is over maximum of %d bytes", size, opts.MaximumSize))
+		}
+
+		// We allow to read an extra byte to check later if we exceed the max size
+		reader = &io.LimitedReader{R: reader, N: opts.MaximumSize + 1}
+	}
+
 	multiWriter := io.MultiWriter(writers...)
 	fh.Size, err = io.Copy(multiWriter, reader)
 	if err != nil {
 		return nil, err
 	}
 
+	if opts.MaximumSize > 0 && fh.Size > opts.MaximumSize {
+		// An extra byte was read thus exceeding the max size
+		return nil, ErrEntityTooLarge
+	}
+
 	if size != -1 && size != fh.Size {
 		return nil, SizeError(fmt.Errorf("expected %d bytes but got only %d", size, fh.Size))
 	}
diff --git a/internal/filestore/file_handler_test.go b/internal/filestore/file_handler_test.go
index 8a096013506026f26ee0c7b91de2bc104248d146..1e11a68aaba7423535d73fbaba609671d99dfca7 100644
--- a/internal/filestore/file_handler_test.go
+++ b/internal/filestore/file_handler_test.go
@@ -69,6 +69,36 @@ func TestSaveFileWrongSize(t *testing.T) {
 	assert.Nil(t, fh)
 }
 
+func TestSaveFileWithKnownSizeExceedLimit(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	tmpFolder, err := ioutil.TempDir("", "workhorse-test-tmp")
+	require.NoError(t, err)
+	defer os.RemoveAll(tmpFolder)
+
+	opts := &filestore.SaveFileOpts{LocalTempPath: tmpFolder, TempFilePrefix: "test-file", MaximumSize: test.ObjectSize - 1}
+	fh, err := filestore.SaveFileFromReader(ctx, strings.NewReader(test.ObjectContent), test.ObjectSize, opts)
+	assert.Error(t, err)
+	_, isSizeError := err.(filestore.SizeError)
+	assert.True(t, isSizeError, "Should fail with SizeError")
+	assert.Nil(t, fh)
+}
+
+func TestSaveFileWithUnknownSizeExceedLimit(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	tmpFolder, err := ioutil.TempDir("", "workhorse-test-tmp")
+	require.NoError(t, err)
+	defer os.RemoveAll(tmpFolder)
+
+	opts := &filestore.SaveFileOpts{LocalTempPath: tmpFolder, TempFilePrefix: "test-file", MaximumSize: test.ObjectSize - 1}
+	fh, err := filestore.SaveFileFromReader(ctx, strings.NewReader(test.ObjectContent), -1, opts)
+	assert.Equal(t, err, filestore.ErrEntityTooLarge)
+	assert.Nil(t, fh)
+}
+
 func TestSaveFromDiskNotExistingFile(t *testing.T) {
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
diff --git a/internal/filestore/save_file_opts.go b/internal/filestore/save_file_opts.go
index 337a20b50333c380de6e5f692abf359ac54fb2b7..1eb708c3f55cc2a6e6a379997ae356b93040b26e 100644
--- a/internal/filestore/save_file_opts.go
+++ b/internal/filestore/save_file_opts.go
@@ -51,6 +51,8 @@ type SaveFileOpts struct {
 	ObjectStorageConfig ObjectStorageConfig
 	// Deadline it the S3 operation deadline, the upload will be aborted if not completed in time
 	Deadline time.Time
+	// The maximum accepted size in bytes of the upload
+	MaximumSize int64
 
 	//MultipartUpload parameters
 	// PartSize is the exact size of each uploaded part. Only the last one can be smaller
@@ -95,6 +97,7 @@ func GetOpts(apiResponse *api.Response) (*SaveFileOpts, error) {
 		UseWorkhorseClient: apiResponse.RemoteObject.UseWorkhorseClient,
 		RemoteTempObjectID: apiResponse.RemoteObject.RemoteTempObjectID,
 		Deadline:           time.Now().Add(timeout),
+		MaximumSize:        apiResponse.MaximumSize,
 	}
 
 	if opts.LocalTempPath != "" && opts.RemoteID != "" {
diff --git a/internal/objectstore/gocloud_object_test.go b/internal/objectstore/gocloud_object_test.go
index 4f79a429a377058fa1814e78b6b3da789376a77f..35b4b84f1a0db017a20b1b8f636bc59fa70879a0 100644
--- a/internal/objectstore/gocloud_object_test.go
+++ b/internal/objectstore/gocloud_object_test.go
@@ -49,22 +49,18 @@ func TestGoCloudObjectUpload(t *testing.T) {
 	require.Equal(t, []byte(test.ObjectContent), received)
 
 	cancel()
-	deleted := false
 
-	retry(3, time.Second, func() error {
+	testhelper.Retry(t, 5*time.Second, func() error {
 		exists, err := bucket.Exists(ctx, objectName)
 		require.NoError(t, err)
 
 		if exists {
-			return fmt.Errorf("file %s is still present, retrying", objectName)
+			return fmt.Errorf("file %s is still present", objectName)
 		} else {
-			deleted = true
 			return nil
 		}
 	})
 
-	require.True(t, deleted)
-
 	// Verify no log noise when deleting a file that already is gone
 	object.Delete()
 	entries := logHook.AllEntries()
diff --git a/internal/objectstore/s3_object_test.go b/internal/objectstore/s3_object_test.go
index f265fe26ca2aeb5fc1e6992d2478092271bae0ce..1f4e530321b6d893a9865f5eb5679c7131f8a3ed 100644
--- a/internal/objectstore/s3_object_test.go
+++ b/internal/objectstore/s3_object_test.go
@@ -19,6 +19,7 @@ import (
 	"gitlab.com/gitlab-org/gitlab-workhorse/internal/config"
 	"gitlab.com/gitlab-org/gitlab-workhorse/internal/objectstore"
 	"gitlab.com/gitlab-org/gitlab-workhorse/internal/objectstore/test"
+	"gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper"
 )
 
 func TestS3ObjectUpload(t *testing.T) {
@@ -59,18 +60,14 @@ func TestS3ObjectUpload(t *testing.T) {
 			test.CheckS3Metadata(t, sess, config, objectName)
 
 			cancel()
-			deleted := false
 
-			retry(3, time.Second, func() error {
+			testhelper.Retry(t, 5*time.Second, func() error {
 				if test.S3ObjectDoesNotExist(t, sess, config, objectName) {
-					deleted = true
 					return nil
-				} else {
-					return fmt.Errorf("file is still present, retrying")
 				}
-			})
 
-			require.True(t, deleted)
+				return fmt.Errorf("file is still present")
+			})
 		})
 	}
 }
@@ -153,23 +150,3 @@ func TestS3ObjectUploadCancel(t *testing.T) {
 	_, err = io.Copy(object, strings.NewReader(test.ObjectContent))
 	require.Error(t, err)
 }
-
-func retry(attempts int, sleep time.Duration, fn func() error) error {
-	if err := fn(); err != nil {
-		if s, ok := err.(stop); ok {
-			// Return the original error for later checking
-			return s.error
-		}
-
-		if attempts--; attempts > 0 {
-			time.Sleep(sleep)
-			return retry(attempts, 2*sleep, fn)
-		}
-		return err
-	}
-	return nil
-}
-
-type stop struct {
-	error
-}
diff --git a/internal/objectstore/uploader.go b/internal/objectstore/uploader.go
index 93a09208ab517b78077b60e3e4e62df65a879192..8fbedf5cc863ad9d35f5122c714fb9ead412dbc8 100644
--- a/internal/objectstore/uploader.go
+++ b/internal/objectstore/uploader.go
@@ -8,6 +8,7 @@ import (
 	"hash"
 	"io"
 	"strings"
+	"sync"
 	"time"
 
 	"gitlab.com/gitlab-org/labkit/log"
@@ -16,6 +17,7 @@ import (
 // Upload represents an upload to an ObjectStorage provider
 type Upload interface {
 	io.WriteCloser
+	CloseWithError(error) error
 	ETag() string
 }
 
@@ -28,7 +30,6 @@ type uploader struct {
 	md5 hash.Hash
 
 	w io.Writer
-	c io.Closer
 
 	// uploadError is the last error occourred during upload
 	uploadError error
@@ -39,25 +40,34 @@ type uploader struct {
 	pw       *io.PipeWriter
 	strategy uploadStrategy
 	metrics  bool
+
+	// closeOnce is used to prevent multiple calls to pw.Close
+	// which may result to Close overriding the error set by CloseWithError
+	// Bug fixed in v1.14: https://github.com/golang/go/commit/f45eb9ff3c96dfd951c65d112d033ed7b5e02432
+	closeOnce sync.Once
 }
 
 func newUploader(strategy uploadStrategy) uploader {
 	pr, pw := io.Pipe()
-	return uploader{w: pw, c: pw, pr: pr, pw: pw, strategy: strategy, metrics: true}
+	return uploader{w: pw, pr: pr, pw: pw, strategy: strategy, metrics: true}
 }
 
 func newMD5Uploader(strategy uploadStrategy, metrics bool) uploader {
 	pr, pw := io.Pipe()
 	hasher := md5.New()
 	mw := io.MultiWriter(pw, hasher)
-	return uploader{w: mw, c: pw, pr: pr, pw: pw, md5: hasher, strategy: strategy, metrics: metrics}
+	return uploader{w: mw, pr: pr, pw: pw, md5: hasher, strategy: strategy, metrics: metrics}
 }
 
 // Close implements the standard io.Closer interface: it closes the http client request.
 // This method will also wait for the connection to terminate and return any error occurred during the upload
 func (u *uploader) Close() error {
-	if err := u.c.Close(); err != nil {
-		return err
+	var closeError error
+	u.closeOnce.Do(func() {
+		closeError = u.pw.Close()
+	})
+	if closeError != nil {
+		return closeError
 	}
 
 	<-u.ctx.Done()
@@ -69,6 +79,14 @@ func (u *uploader) Close() error {
 	return u.uploadError
 }
 
+func (u *uploader) CloseWithError(err error) error {
+	u.closeOnce.Do(func() {
+		u.pw.CloseWithError(err)
+	})
+
+	return nil
+}
+
 func (u *uploader) Write(p []byte) (int, error) {
 	return u.w.Write(p)
 }
diff --git a/internal/testhelper/testhelper.go b/internal/testhelper/testhelper.go
index ab4e7e658f619ce6c21521cdfc235d555cb4cca4..67b72e8d2082c330e2055400ca9190c47fbad705 100644
--- a/internal/testhelper/testhelper.go
+++ b/internal/testhelper/testhelper.go
@@ -161,3 +161,16 @@ func WaitForLogEvent(hook *test.Hook) bool {
 
 	return false
 }
+
+func Retry(t testing.TB, timeout time.Duration, fn func() error) {
+	t.Helper()
+	start := time.Now()
+	var err error
+	for ; time.Since(start) < timeout; time.Sleep(time.Millisecond) {
+		err = fn()
+		if err == nil {
+			return
+		}
+	}
+	t.Fatalf("test timeout after %v; last error: %v", timeout, err)
+}