diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index 9e08d00f0d..f27c28ad02 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -187,7 +187,7 @@ func (repo *Repository) GetTag(name string) (*Tag, error) {
 }
 
 // GetTagInfos returns all tag infos of the repository.
-func (repo *Repository) GetTagInfos() ([]*Tag, error) {
+func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, error) {
 	// TODO this a slow implementation, makes one git command per tag
 	stdout, err := NewCommand("tag").RunInDir(repo.Path)
 	if err != nil {
@@ -195,6 +195,18 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
 	}
 
 	tagNames := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
+
+	if page != 0 {
+		skip := (page - 1) * pageSize
+		if skip >= len(tagNames) {
+			return nil, nil
+		}
+		if (len(tagNames) - skip) < pageSize {
+			pageSize = len(tagNames) - skip
+		}
+		tagNames = tagNames[skip : skip+pageSize]
+	}
+
 	var tags = make([]*Tag, 0, len(tagNames))
 	for _, tagName := range tagNames {
 		tagName = strings.TrimSpace(tagName)
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
index 90f2b37358..7a644e39db 100644
--- a/modules/git/repo_tag_test.go
+++ b/modules/git/repo_tag_test.go
@@ -18,7 +18,7 @@ func TestRepository_GetTags(t *testing.T) {
 	assert.NoError(t, err)
 	defer bareRepo1.Close()
 
-	tags, err := bareRepo1.GetTagInfos()
+	tags, err := bareRepo1.GetTagInfos(0, 0)
 	assert.NoError(t, err)
 	assert.Len(t, tags, 1)
 	assert.EqualValues(t, "test", tags[0].Name)
diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go
index eb99895c64..76c612bea4 100644
--- a/routers/api/v1/repo/tag.go
+++ b/routers/api/v1/repo/tag.go
@@ -10,6 +10,7 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
 	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/routers/api/v1/utils"
 )
 
 // ListTags list all the tags of a repository
@@ -30,11 +31,21 @@ func ListTags(ctx *context.APIContext) {
 	//   description: name of the repo
 	//   type: string
 	//   required: true
+	// - name: page
+	//   in: query
+	//   description: page number of results to return (1-based)
+	//   type: integer
+	// - name: limit
+	//   in: query
+	//   description: page size of results, default maximum page size is 50
+	//   type: integer
 	// responses:
 	//   "200":
 	//     "$ref": "#/responses/TagList"
 
-	tags, err := ctx.Repo.GitRepo.GetTagInfos()
+	listOpts := utils.GetListOptions(ctx)
+
+	tags, err := ctx.Repo.GitRepo.GetTagInfos(listOpts.Page, listOpts.PageSize)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "GetTags", err)
 		return
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index b52145a0a9..bac1f05710 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -7251,6 +7251,18 @@
             "name": "repo",
             "in": "path",
             "required": true
+          },
+          {
+            "type": "integer",
+            "description": "page number of results to return (1-based)",
+            "name": "page",
+            "in": "query"
+          },
+          {
+            "type": "integer",
+            "description": "page size of results, default maximum page size is 50",
+            "name": "limit",
+            "in": "query"
           }
         ],
         "responses": {