diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go
index 1486e83c57db..07e34da8e2df 100644
--- a/routers/api/packages/maven/maven.go
+++ b/routers/api/packages/maven/maven.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 
 	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/globallock"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -223,6 +224,10 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool
 	helper.ServePackageFile(ctx, s, u, pf, opts)
 }
 
+func mavenPkgNameKey(packageName string) string {
+	return "pkg_maven_" + packageName
+}
+
 // UploadPackageFile adds a file to the package. If the package does not exist, it gets created.
 func UploadPackageFile(ctx *context.Context) {
 	params, err := extractPathParameters(ctx)
@@ -241,6 +246,14 @@ func UploadPackageFile(ctx *context.Context) {
 
 	packageName := params.GroupID + "-" + params.ArtifactID
 
+	// for the same package, only one upload at a time
+	releaser, err := globallock.Lock(ctx, mavenPkgNameKey(packageName))
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer releaser()
+
 	buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
 	if err != nil {
 		apiError(ctx, http.StatusInternalServerError, err)
diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go
index 0466a727b25f..e54238858c2e 100644
--- a/tests/integration/api_packages_maven_test.go
+++ b/tests/integration/api_packages_maven_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"strconv"
 	"strings"
+	"sync"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
@@ -252,3 +253,35 @@ func TestPackageMaven(t *testing.T) {
 		assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
 	})
 }
+
+func TestPackageMavenConcurrent(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+	groupID := "com.gitea"
+	artifactID := "test-project"
+	packageVersion := "1.0.1"
+
+	root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID)
+
+	putFile := func(t *testing.T, path, content string, expectedStatus int) {
+		req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content)).
+			AddBasicAuth(user.Name)
+		MakeRequest(t, req, expectedStatus)
+	}
+
+	t.Run("Concurrent Upload", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		var wg sync.WaitGroup
+		for i := 0; i < 10; i++ {
+			wg.Add(1)
+			go func(i int) {
+				putFile(t, fmt.Sprintf("/%s/%s.jar", packageVersion, strconv.Itoa(i)), "test", http.StatusCreated)
+				wg.Done()
+			}(i)
+		}
+		wg.Wait()
+	})
+}