diff --git a/internal/arduino/builder/internal/preprocessor/gcc.go b/internal/arduino/builder/internal/preprocessor/gcc.go
index cbf156dfae6..f8d6100d10d 100644
--- a/internal/arduino/builder/internal/preprocessor/gcc.go
+++ b/internal/arduino/builder/internal/preprocessor/gcc.go
@@ -16,6 +16,7 @@
 package preprocessor
 
 import (
+	"bytes"
 	"context"
 	"errors"
 	"fmt"
@@ -77,10 +78,42 @@ func GCC(
 	if err != nil {
 		return Result{}, err
 	}
-	stdout, stderr, err := proc.RunAndCaptureOutput(ctx)
 
-	// Append gcc arguments to stdout
-	stdout = append([]byte(fmt.Sprintln(strings.Join(args, " "))), stdout...)
+	stdout := bytes.NewBuffer(nil)
+	stderr := bytes.NewBuffer(nil)
 
-	return Result{args: proc.GetArgs(), stdout: stdout, stderr: stderr}, err
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+	count := 0
+	stderrLimited := writerFunc(func(p []byte) (int, error) {
+		// Limit the size of the stderr buffer to 100KB
+		n, err := stderr.Write(p)
+		count += n
+		if count > 100*1024 {
+			fmt.Fprintln(stderr, i18n.Tr("Compiler error output has been truncated."))
+			cancel()
+		}
+		return n, err
+	})
+
+	proc.RedirectStdoutTo(stdout)
+	proc.RedirectStderrTo(stderrLimited)
+
+	// Append gcc arguments to stdout before running the command
+	fmt.Fprintln(stdout, strings.Join(args, " "))
+
+	if err := proc.Start(); err != nil {
+		return Result{}, err
+	}
+
+	// Wait for the process to finish
+	err = proc.WaitWithinContext(ctx)
+
+	return Result{args: proc.GetArgs(), stdout: stdout.Bytes(), stderr: stderr.Bytes()}, err
+}
+
+type writerFunc func(p []byte) (n int, err error)
+
+func (f writerFunc) Write(p []byte) (n int, err error) {
+	return f(p)
 }
diff --git a/internal/integrationtest/arduino-cli.go b/internal/integrationtest/arduino-cli.go
index f035e792f55..4b2b4850450 100644
--- a/internal/integrationtest/arduino-cli.go
+++ b/internal/integrationtest/arduino-cli.go
@@ -22,6 +22,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"maps"
 	"os"
 	"runtime"
 	"strings"
@@ -190,12 +191,16 @@ func (cli *ArduinoCLI) Run(args ...string) ([]byte, []byte, error) {
 	return cli.RunWithCustomEnv(cli.cliEnvVars, args...)
 }
 
+// RunWithContext executes the given arduino-cli command with the given context and returns the output.
+// If the context is canceled, the command is killed.
+func (cli *ArduinoCLI) RunWithContext(ctx context.Context, args ...string) ([]byte, []byte, error) {
+	return cli.RunWithCustomEnvContext(ctx, cli.cliEnvVars, args...)
+}
+
 // GetDefaultEnv returns a copy of the default execution env used with the Run method.
 func (cli *ArduinoCLI) GetDefaultEnv() map[string]string {
 	res := map[string]string{}
-	for k, v := range cli.cliEnvVars {
-		res[k] = v
-	}
+	maps.Copy(res, cli.cliEnvVars)
 	return res
 }
 
@@ -324,8 +329,13 @@ func (cli *ArduinoCLI) InstallMockedAvrdude(t *testing.T) {
 
 // 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) {
+	return cli.RunWithCustomEnvContext(context.Background(), env, args...)
+}
+
+// RunWithCustomEnv executes the given arduino-cli command with the given custom env and returns the output.
+func (cli *ArduinoCLI) RunWithCustomEnvContext(ctx context.Context, env map[string]string, args ...string) ([]byte, []byte, error) {
 	var stdoutBuf, stderrBuf bytes.Buffer
-	err := cli.run(&stdoutBuf, &stderrBuf, nil, env, args...)
+	err := cli.run(ctx, &stdoutBuf, &stderrBuf, nil, env, args...)
 
 	errBuf := stderrBuf.Bytes()
 	cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")
@@ -336,7 +346,7 @@ func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) (
 // RunWithCustomInput executes the given arduino-cli command pushing the given input stream and returns the output.
 func (cli *ArduinoCLI) RunWithCustomInput(in io.Reader, args ...string) ([]byte, []byte, error) {
 	var stdoutBuf, stderrBuf bytes.Buffer
-	err := cli.run(&stdoutBuf, &stderrBuf, in, cli.cliEnvVars, args...)
+	err := cli.run(context.Background(), &stdoutBuf, &stderrBuf, in, cli.cliEnvVars, args...)
 
 	errBuf := stderrBuf.Bytes()
 	cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")
@@ -344,7 +354,7 @@ func (cli *ArduinoCLI) RunWithCustomInput(in io.Reader, args ...string) ([]byte,
 	return stdoutBuf.Bytes(), errBuf, err
 }
 
-func (cli *ArduinoCLI) run(stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader, env map[string]string, args ...string) error {
+func (cli *ArduinoCLI) run(ctx context.Context, stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader, env map[string]string, args ...string) error {
 	if cli.cliConfigPath != nil {
 		args = append([]string{"--config-file", cli.cliConfigPath.String()}, args...)
 	}
@@ -402,8 +412,8 @@ func (cli *ArduinoCLI) run(stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader
 			}
 		}()
 	}
+	cliErr := cliProc.WaitWithinContext(ctx)
 	wg.Wait()
-	cliErr := cliProc.Wait()
 	fmt.Fprintln(terminalOut, color.HiBlackString("<<< Run completed (err = %v)", cliErr))
 
 	return cliErr
diff --git a/internal/integrationtest/compile_5/recusive_include_test.go b/internal/integrationtest/compile_5/recusive_include_test.go
new file mode 100644
index 00000000000..59a3c421c7f
--- /dev/null
+++ b/internal/integrationtest/compile_5/recusive_include_test.go
@@ -0,0 +1,44 @@
+// This file is part of arduino-cli.
+//
+// Copyright 2025 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 compile_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/arduino/arduino-cli/internal/integrationtest"
+	"github.com/arduino/go-paths-helper"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCompileWithInfiniteMultipleIncludeRecursion(t *testing.T) {
+	env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
+	t.Cleanup(env.CleanUp)
+
+	// Install Arduino AVR Boards
+	_, _, err := cli.Run("core", "install", "arduino:avr@1.8.6")
+	require.NoError(t, err)
+
+	sketch, err := paths.New("testdata", "SketchWithRecursiveIncludes").Abs()
+	require.NoError(t, err)
+
+	// Time-limited test to prevent OOM
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
+	t.Cleanup(cancel)
+	_, _, _ = cli.RunWithContext(ctx, "compile", "-b", "arduino:avr:uno", sketch.String())
+	require.NotErrorIs(t, ctx.Err(), context.DeadlineExceeded, "compilation should not hang")
+}
diff --git a/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/SketchWithRecursiveIncludes.ino b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/SketchWithRecursiveIncludes.ino
new file mode 100644
index 00000000000..2243de1baf9
--- /dev/null
+++ b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/SketchWithRecursiveIncludes.ino
@@ -0,0 +1 @@
+#include "a.h"
diff --git a/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/a.h b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/a.h
new file mode 100644
index 00000000000..7c626e5beb0
--- /dev/null
+++ b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/a.h
@@ -0,0 +1,2 @@
+#include "a.h"
+#include "a.h"