diff --git a/commands/service_upload.go b/commands/service_upload.go
index 3ebc76a9804..56d621813fe 100644
--- a/commands/service_upload.go
+++ b/commands/service_upload.go
@@ -573,18 +573,18 @@ func (s *arduinoCoreServerImpl) runProgramAction(ctx context.Context, pme *packa
 	// Run recipes for upload
 	toolEnv := pme.GetEnvVarsForSpawnedProcess()
 	if burnBootloader {
-		if err := runTool("erase.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
+		if err := runTool(uploadCtx, "erase.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
 			return nil, &cmderrors.FailedUploadError{Message: i18n.Tr("Failed chip erase"), Cause: err}
 		}
-		if err := runTool("bootloader.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
+		if err := runTool(uploadCtx, "bootloader.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
 			return nil, &cmderrors.FailedUploadError{Message: i18n.Tr("Failed to burn bootloader"), Cause: err}
 		}
 	} else if programmer != nil {
-		if err := runTool("program.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
+		if err := runTool(uploadCtx, "program.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
 			return nil, &cmderrors.FailedUploadError{Message: i18n.Tr("Failed programming"), Cause: err}
 		}
 	} else {
-		if err := runTool("upload.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
+		if err := runTool(uploadCtx, "upload.pattern", uploadProperties, outStream, errStream, verbose, dryRun, toolEnv); err != nil {
 			return nil, &cmderrors.FailedUploadError{Message: i18n.Tr("Failed uploading"), Cause: err}
 		}
 	}
@@ -702,7 +702,12 @@ func detectUploadPort(
 	}
 }
 
-func runTool(recipeID string, props *properties.Map, outStream, errStream io.Writer, verbose bool, dryRun bool, toolEnv []string) error {
+func runTool(ctx context.Context, recipeID string, props *properties.Map, outStream, errStream io.Writer, verbose bool, dryRun bool, toolEnv []string) error {
+	// if ctx is already canceled just exit
+	if err := ctx.Err(); err != nil {
+		return err
+	}
+
 	recipe, ok := props.GetOk(recipeID)
 	if !ok {
 		return errors.New(i18n.Tr("recipe not found '%s'", recipeID))
@@ -739,6 +744,17 @@ func runTool(recipeID string, props *properties.Map, outStream, errStream io.Wri
 		return errors.New(i18n.Tr("cannot execute upload tool: %s", err))
 	}
 
+	// If the ctx is canceled, kill the running command
+	completed := make(chan struct{})
+	defer close(completed)
+	go func() {
+		select {
+		case <-ctx.Done():
+			_ = cmd.Kill()
+		case <-completed:
+		}
+	}()
+
 	if err := cmd.Wait(); err != nil {
 		return errors.New(i18n.Tr("uploading error: %s", err))
 	}
diff --git a/internal/integrationtest/arduino-cli.go b/internal/integrationtest/arduino-cli.go
index 62eaa27ee08..231065843d4 100644
--- a/internal/integrationtest/arduino-cli.go
+++ b/internal/integrationtest/arduino-cli.go
@@ -286,6 +286,42 @@ func (cli *ArduinoCLI) InstallMockedSerialMonitor(t *testing.T) {
 	}
 }
 
+// InstallMockedAvrdude will replace the already installed avrdude with a mocked one.
+func (cli *ArduinoCLI) InstallMockedAvrdude(t *testing.T) {
+	fmt.Println(color.BlueString("<<< Install mocked avrdude"))
+
+	// Build mocked serial-discovery
+	mockDir := FindRepositoryRootPath(t).Join("internal", "mock_avrdude")
+	gobuild, err := paths.NewProcess(nil, "go", "build")
+	require.NoError(t, err)
+	gobuild.SetDirFromPath(mockDir)
+	require.NoError(t, gobuild.Run(), "Building mocked avrdude")
+	ext := ""
+	if runtime.GOOS == "windows" {
+		ext = ".exe"
+	}
+	mockBin := mockDir.Join("mock_avrdude" + ext)
+	require.True(t, mockBin.Exist())
+	fmt.Println(color.HiBlackString("    Build of mocked avrdude succeeded."))
+
+	// Install it replacing the current avrdudes
+	dataDir := cli.DataDir()
+	require.NotNil(t, dataDir, "data dir missing")
+
+	avrdudes, err := dataDir.Join("packages", "arduino", "tools", "avrdude").ReadDirRecursiveFiltered(
+		nil, paths.AndFilter(
+			paths.FilterNames("avrdude"+ext),
+			paths.FilterOutDirectories(),
+		),
+	)
+	require.NoError(t, err, "scanning data dir for avrdude(s)")
+	require.NotEmpty(t, avrdudes, "no avrdude(s) found in data dir")
+	for _, avrdude := range avrdudes {
+		require.NoError(t, mockBin.CopyTo(avrdude), "installing mocked avrdude to %s", avrdude)
+		fmt.Println(color.HiBlackString("    Mocked avrdude installed in " + avrdude.String()))
+	}
+}
+
 // RunWithCustomEnv executes the given arduino-cli command with the given custom env and returns the output.
 func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) ([]byte, []byte, error) {
 	var stdoutBuf, stderrBuf bytes.Buffer
@@ -642,3 +678,19 @@ func (inst *ArduinoCLIInstance) Monitor(ctx context.Context, port *commands.Port
 	})
 	return monitorClient, err
 }
+
+// Upload calls the "Upload" gRPC method.
+func (inst *ArduinoCLIInstance) Upload(ctx context.Context, fqbn, sketchPath, port, protocol string) (commands.ArduinoCoreService_UploadClient, error) {
+	uploadCl, err := inst.cli.daemonClient.Upload(ctx, &commands.UploadRequest{
+		Instance:   inst.instance,
+		Fqbn:       fqbn,
+		SketchPath: sketchPath,
+		Verbose:    true,
+		Port: &commands.Port{
+			Address:  port,
+			Protocol: protocol,
+		},
+	})
+	logCallf(">>> Upload(%v %v port/protocol=%s/%s)\n", fqbn, sketchPath, port, protocol)
+	return uploadCl, err
+}
diff --git a/internal/integrationtest/daemon/upload_test.go b/internal/integrationtest/daemon/upload_test.go
new file mode 100644
index 00000000000..75c731aab09
--- /dev/null
+++ b/internal/integrationtest/daemon/upload_test.go
@@ -0,0 +1,120 @@
+// This file is part of arduino-cli.
+//
+// Copyright 2024 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 (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/arduino/arduino-cli/internal/integrationtest"
+	"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
+	"github.com/arduino/go-paths-helper"
+	"github.com/stretchr/testify/require"
+)
+
+func TestUploadCancelation(t *testing.T) {
+	env, cli := integrationtest.CreateEnvForDaemon(t)
+	defer env.CleanUp()
+
+	grpcInst := cli.Create()
+	require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
+		fmt.Printf("INIT> %v\n", ir.GetMessage())
+	}))
+
+	plInst, err := grpcInst.PlatformInstall(context.Background(), "arduino", "avr", "1.8.6", true)
+	require.NoError(t, err)
+	for {
+		msg, err := plInst.Recv()
+		if errors.Is(err, io.EOF) {
+			break
+		}
+		require.NoError(t, err)
+		fmt.Printf("INSTALL> %v\n", msg)
+	}
+
+	// Mock avrdude
+	cli.InstallMockedAvrdude(t)
+
+	// Re-init instance to update changes
+	require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
+		fmt.Printf("INIT> %v\n", ir.GetMessage())
+	}))
+
+	// Build sketch for upload
+	sk := paths.New("testdata", "bare_minimum")
+	compile, err := grpcInst.Compile(context.Background(), "arduino:avr:uno", sk.String(), "")
+	require.NoError(t, err)
+	for {
+		msg, err := compile.Recv()
+		if errors.Is(err, io.EOF) {
+			break
+		}
+		if err != nil {
+			fmt.Println("COMPILE ERROR>", err)
+			require.FailNow(t, "Expected successful compile", "compilation failed")
+			break
+		}
+		if msg.GetOutStream() != nil {
+			fmt.Printf("COMPILE OUT> %v\n", string(msg.GetOutStream()))
+		}
+		if msg.GetErrStream() != nil {
+			fmt.Printf("COMPILE ERR> %v\n", string(msg.GetErrStream()))
+		}
+	}
+
+	// Try upload and interrupt the call after 1 sec
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+	upload, err := grpcInst.Upload(ctx, "arduino:avr:uno", sk.String(), "/dev/ttyACM0", "serial")
+	require.NoError(t, err)
+	checkFile := ""
+	for {
+		msg, err := upload.Recv()
+		if errors.Is(err, io.EOF) {
+			require.FailNow(t, "Expected interrupted upload", "upload succeeded")
+			break
+		}
+		if err != nil {
+			fmt.Println("UPLOAD ERROR>", err)
+			break
+		}
+		if out := string(msg.GetOutStream()); out != "" {
+			fmt.Printf("UPLOAD OUT> %v\n", out)
+			if strings.HasPrefix(out, "CHECKFILE: ") {
+				checkFile = strings.TrimSpace(out[11:])
+			}
+		}
+		if msg.GetErrStream() != nil {
+			fmt.Printf("UPLOAD ERR> %v\n", string(msg.GetErrStream()))
+		}
+	}
+	cancel()
+
+	// Wait 5 seconds.
+	// If the mocked avrdude is not killed it will create a checkfile and it will remove it after 5 seconds.
+	time.Sleep(5 * time.Second)
+
+	// Test if the checkfile is still there (if the file is there it means that mocked avrdude
+	// has been correctly killed).
+	require.NotEmpty(t, checkFile)
+	require.FileExists(t, checkFile)
+	require.NoError(t, os.Remove(checkFile))
+}
diff --git a/internal/mock_avrdude/.gitignore b/internal/mock_avrdude/.gitignore
new file mode 100644
index 00000000000..7035844ce4e
--- /dev/null
+++ b/internal/mock_avrdude/.gitignore
@@ -0,0 +1 @@
+mock_avrdude
diff --git a/internal/mock_avrdude/main.go b/internal/mock_avrdude/main.go
new file mode 100644
index 00000000000..3b10a1d0207
--- /dev/null
+++ b/internal/mock_avrdude/main.go
@@ -0,0 +1,46 @@
+//
+// This file is part arduino-cli.
+//
+// Copyright 2023 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 main
+
+import (
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/arduino/go-paths-helper"
+)
+
+func main() {
+	tmp, err := paths.MkTempFile(nil, "test")
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+	tmp.Close()
+	tmpPath := paths.New(tmp.Name())
+
+	fmt.Println("CHECKFILE:", tmpPath)
+
+	// Just sit here for 5 seconds
+	time.Sleep(5 * time.Second)
+
+	// Remove the check file at the end
+	tmpPath.Remove()
+
+	fmt.Println("COMPLETED")
+}