diff --git a/internal/integrationtest/daemon/daemon_test.go b/internal/integrationtest/daemon/daemon_test.go
index 2c10183eaa1..cbb227293c4 100644
--- a/internal/integrationtest/daemon/daemon_test.go
+++ b/internal/integrationtest/daemon/daemon_test.go
@@ -23,6 +23,7 @@ import (
 	"time"
 
 	"github.com/arduino/arduino-cli/arduino"
+	f "github.com/arduino/arduino-cli/internal/algorithms"
 	"github.com/arduino/arduino-cli/internal/integrationtest"
 	"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
 	"github.com/arduino/go-paths-helper"
@@ -171,6 +172,7 @@ func TestDaemonCompileOptions(t *testing.T) {
 	// Build sketch (without errors)
 	compile, err = grpcInst.Compile(context.Background(), "arduino:avr:uno:some_menu=good", sk.String(), "")
 	require.NoError(t, err)
+	analyzer := NewTaskProgressAnalyzer(t)
 	for {
 		msg, err := compile.Recv()
 		if err == io.EOF {
@@ -180,7 +182,13 @@ func TestDaemonCompileOptions(t *testing.T) {
 		if msg.ErrStream != nil {
 			fmt.Printf("COMPILE> %v\n", string(msg.GetErrStream()))
 		}
+		analyzer.Process(msg.GetProgress())
 	}
+	// https://github.com/arduino/arduino-cli/issues/2016
+	// assert that the task progress is increasing and doesn't contain multiple 100% values
+	results := analyzer.Results[""]
+	require.True(t, results[len(results)-1].Completed)
+	require.IsNonDecreasing(t, f.Map(results, (*commands.TaskProgress).GetPercent))
 }
 
 func TestDaemonCompileAfterFailedLibInstall(t *testing.T) {
diff --git a/internal/integrationtest/daemon/task_progress_test.go b/internal/integrationtest/daemon/task_progress_test.go
new file mode 100644
index 00000000000..c106a2c6588
--- /dev/null
+++ b/internal/integrationtest/daemon/task_progress_test.go
@@ -0,0 +1,46 @@
+// This file is part of arduino-cli.
+//
+// Copyright 2022 ARDUINO SA (http://www.arduino.cc/)
+//
+// This software is released under the GNU General Public License version 3,
+// which covers the main part of arduino-cli.
+// The terms of this license can be found at:
+// https://www.gnu.org/licenses/gpl-3.0.en.html
+//
+// You can be released from the requirements of the above licenses by purchasing
+// a commercial license. Buying such a license is mandatory if you want to
+// modify or otherwise use the software for commercial activities involving the
+// Arduino software without disclosing the source code of your own applications.
+// To purchase a commercial license, send an email to license@arduino.cc.
+
+package daemon_test
+
+import (
+	"testing"
+
+	"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
+)
+
+// TaskProgressAnalyzer analyzes TaskProgress messages for consistency
+type TaskProgressAnalyzer struct {
+	t       *testing.T
+	Results map[string][]*commands.TaskProgress
+}
+
+// NewTaskProgressAnalyzer creates a new TaskProgressAnalyzer
+func NewTaskProgressAnalyzer(t *testing.T) *TaskProgressAnalyzer {
+	return &TaskProgressAnalyzer{
+		t:       t,
+		Results: map[string][]*commands.TaskProgress{},
+	}
+}
+
+// Process the given TaskProgress message.
+func (a *TaskProgressAnalyzer) Process(progress *commands.TaskProgress) {
+	if progress == nil {
+		return
+	}
+
+	taskName := progress.GetName()
+	a.Results[taskName] = append(a.Results[taskName], progress)
+}
diff --git a/legacy/builder/builder.go b/legacy/builder/builder.go
index fe11d684faa..90d92938811 100644
--- a/legacy/builder/builder.go
+++ b/legacy/builder/builder.go
@@ -41,7 +41,7 @@ func (s *Builder) Run(ctx *types.Context) error {
 		return err
 	}
 
-	var _err error
+	var _err, mainErr error
 	commands := []types.Command{
 		&ContainerSetupHardwareToolsLibsSketchAndProps{},
 
@@ -92,12 +92,25 @@ func (s *Builder) Run(ctx *types.Context) error {
 		&RecipeByPrefixSuffixRunner{Prefix: "recipe.hooks.postbuild", Suffix: ".pattern", SkipIfOnlyUpdatingCompilationDatabase: true},
 	}
 
-	mainErr := runCommands(ctx, commands)
+	ctx.Progress.AddSubSteps(len(commands) + 4)
+	defer ctx.Progress.RemoveSubSteps()
+
+	for _, command := range commands {
+		PrintRingNameIfDebug(ctx, command)
+		err := command.Run(ctx)
+		if err != nil {
+			mainErr = errors.WithStack(err)
+			break
+		}
+		ctx.Progress.CompleteStep()
+		ctx.PushProgress()
+	}
 
 	if ctx.CompilationDatabase != nil {
 		ctx.CompilationDatabase.SaveToFile()
 	}
 
+	var otherErr error
 	commands = []types.Command{
 		&PrintUsedAndNotUsedLibraries{SketchError: mainErr != nil},
 
@@ -107,7 +120,16 @@ func (s *Builder) Run(ctx *types.Context) error {
 
 		&phases.Sizer{SketchError: mainErr != nil},
 	}
-	otherErr := runCommands(ctx, commands)
+	for _, command := range commands {
+		PrintRingNameIfDebug(ctx, command)
+		err := command.Run(ctx)
+		if err != nil {
+			otherErr = errors.WithStack(err)
+			break
+		}
+		ctx.Progress.CompleteStep()
+		ctx.PushProgress()
+	}
 
 	if mainErr != nil {
 		return mainErr
@@ -193,8 +215,7 @@ func PrintRingNameIfDebug(ctx *types.Context, command types.Command) {
 }
 
 func RunBuilder(ctx *types.Context) error {
-	command := Builder{}
-	return command.Run(ctx)
+	return runCommands(ctx, []types.Command{&Builder{}})
 }
 
 func RunParseHardware(ctx *types.Context) error {
diff --git a/legacy/builder/container_setup.go b/legacy/builder/container_setup.go
index dd52af49b8f..3a00ff6edaa 100644
--- a/legacy/builder/container_setup.go
+++ b/legacy/builder/container_setup.go
@@ -23,14 +23,13 @@ import (
 type ContainerSetupHardwareToolsLibsSketchAndProps struct{}
 
 func (s *ContainerSetupHardwareToolsLibsSketchAndProps) Run(ctx *types.Context) error {
-	// total number of steps in this container: 4
-	ctx.Progress.AddSubSteps(4)
-	defer ctx.Progress.RemoveSubSteps()
 	commands := []types.Command{
 		&AddAdditionalEntriesToContext{},
 		&FailIfBuildPathEqualsSketchPath{},
 		&LibrariesLoader{},
 	}
+	ctx.Progress.AddSubSteps(len(commands))
+	defer ctx.Progress.RemoveSubSteps()
 	for _, command := range commands {
 		PrintRingNameIfDebug(ctx, command)
 		err := command.Run(ctx)
diff --git a/legacy/builder/types/context.go b/legacy/builder/types/context.go
index 8bbd05f7308..0a160324a46 100644
--- a/legacy/builder/types/context.go
+++ b/legacy/builder/types/context.go
@@ -200,7 +200,10 @@ func (ctx *Context) ExtractBuildOptions() *properties.Map {
 
 func (ctx *Context) PushProgress() {
 	if ctx.ProgressCB != nil {
-		ctx.ProgressCB(&rpc.TaskProgress{Percent: ctx.Progress.Progress})
+		ctx.ProgressCB(&rpc.TaskProgress{
+			Percent:   ctx.Progress.Progress,
+			Completed: ctx.Progress.Progress >= 100.0,
+		})
 	}
 }