diff --git a/workhorse/_support/lint_last_known_acceptable.txt b/workhorse/_support/lint_last_known_acceptable.txt
index b17c131af6af24a4bf70a66c7cdc0fe71ad1ce89..3e512821081a305f0d457e02b110b4bb66d74307 100644
--- a/workhorse/_support/lint_last_known_acceptable.txt
+++ b/workhorse/_support/lint_last_known_acceptable.txt
@@ -13,9 +13,8 @@ internal/git/archive.go:67: Function 'Inject' has too many statements (55 > 40)
 internal/git/blob.go:21:5: exported: exported var SendBlob should have comment or be unexported (revive)
 internal/git/diff.go:1: 1-47 lines are duplicate of `internal/git/format-patch.go:1-48` (dupl)
 internal/git/diff.go:22:5: exported: exported var SendDiff should have comment or be unexported (revive)
-internal/git/error.go:36:4: singleCaseSwitch: should rewrite switch statement to if statement (gocritic)
-internal/git/error_test.go:28: File is not `gofmt`-ed with `-s` (gofmt)
-internal/git/error_test.go:66:2: unnecessary trailing newline (whitespace)
+internal/git/error.go:29:45: context-as-argument: context.Context should be the first parameter of a function (revive)
+internal/git/error.go:38:4: singleCaseSwitch: should rewrite switch statement to if statement (gocritic)
 internal/git/format-patch.go:1: 1-48 lines are duplicate of `internal/git/diff.go:1-47` (dupl)
 internal/git/format-patch.go:22:5: exported: exported var SendPatch should have comment or be unexported (revive)
 internal/git/git-http.go:21:2: exported: comment on exported const GitConfigShowAllRefs should be of the form "GitConfigShowAllRefs ..." (revive)
diff --git a/workhorse/internal/git/error.go b/workhorse/internal/git/error.go
index 311c76a01d09e5bbc890c8b01fdc51fff954a187..05a48619009d4f4860f6ee372abb5abf4eb819ed 100644
--- a/workhorse/internal/git/error.go
+++ b/workhorse/internal/git/error.go
@@ -1,11 +1,13 @@
 package git
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"io"
 
 	"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
+	"gitlab.com/gitlab-org/labkit/correlation"
 	"google.golang.org/grpc/status"
 
 	"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
@@ -24,7 +26,7 @@ type copyError struct{ error }
 // LimitError. A LimitError is returned by Gitaly when it is at its limit in
 // handling requests. Since this is a known error, we should print a sensible
 // error message to the end user.
-func handleLimitErr(err error, w io.Writer, f func(w io.Writer) error) {
+func handleLimitErr(err error, w io.Writer, c context.Context, f func(w io.Writer, correlationID string) error) {
 	var statusErr grpcErr
 	if !errors.As(err, &statusErr) {
 		return
@@ -35,7 +37,7 @@ func handleLimitErr(err error, w io.Writer, f func(w io.Writer) error) {
 		for _, detail := range details {
 			switch detail.(type) {
 			case *gitalypb.LimitError:
-				if err := f(w); err != nil {
+				if err := f(w, correlation.ExtractFromContext(c)); err != nil {
 					log.WithError(fmt.Errorf("handling limit error: %w", err))
 				}
 			}
@@ -54,7 +56,7 @@ func handleLimitErr(err error, w io.Writer, f func(w io.Writer) error) {
 // information through the side-band 2 channel.
 // See https://gitlab.com/gitlab-org/gitaly/-/tree/jc-return-structured-error-limits
 // for more details.
-func writeReceivePackError(w io.Writer) error {
+func writeReceivePackError(w io.Writer, correlationID string) error {
 	if _, err := fmt.Fprintf(w, "%04x", 35); err != nil {
 		return err
 	}
@@ -71,15 +73,10 @@ func writeReceivePackError(w io.Writer) error {
 		return err
 	}
 
-	if _, err := fmt.Fprintf(w, "%04x", 68); err != nil {
-		return err
-	}
-
-	if _, err := w.Write([]byte{0x2}); err != nil {
-		return err
-	}
+	correlationID = truncateCorrelationID(correlationID)
+	msg := fmt.Sprintf("\x02GitLab is currently unable to handle this request due to load (ID %s).\n", correlationID)
 
-	if _, err := fmt.Fprint(w, "GitLab is currently unable to handle this request due to load.\n"); err != nil {
+	if err := writeMessage(w, msg); err != nil {
 		return err
 	}
 
@@ -94,7 +91,27 @@ func writeReceivePackError(w io.Writer) error {
 // understands and prints to stdout. UploadPack expects to receive pack data in
 // PKT-LINE format. An error-line can be passed that begins with ERR.
 // See https://git-scm.com/docs/pack-protocol/2.29.0#_pkt_line_format.
-func writeUploadPackError(w io.Writer) error {
-	_, err := fmt.Fprintf(w, "%04xERR GitLab is currently unable to handle this request due to load.\n", 71)
+func writeUploadPackError(w io.Writer, correlationID string) error {
+	correlationID = truncateCorrelationID(correlationID)
+	msg := fmt.Sprintf("ERR GitLab is currently unable to handle this request due to load (ID %s).\n", correlationID)
+	return writeMessage(w, msg)
+}
+
+func truncateCorrelationID(correlationID string) string {
+	// We often use ULIDs (Universally Unique Lexicographically Sortable Identifiers,
+	// see https://github.com/oklog/ulid) that are encoded to 26 chars but some
+	// correlation IDs may be 32 characters. To be future proof, we’re limiting this
+	// to more than that because e.g. a UUID is 36 characters already.
+	const maxCorrelationIDLen = 48
+
+	if len(correlationID) > maxCorrelationIDLen {
+		correlationID = correlationID[:maxCorrelationIDLen] + " (truncated)"
+	}
+
+	return correlationID
+}
+
+func writeMessage(w io.Writer, msg string) error {
+	_, err := fmt.Fprintf(w, "%04x%s", len(msg)+4, msg)
 	return err
 }
diff --git a/workhorse/internal/git/error_test.go b/workhorse/internal/git/error_test.go
index af39522c20d24a39a2fa569bee4462e24112b1d1..5fbc0dd336d2bdc16e59cbf1dc1749b10addab07 100644
--- a/workhorse/internal/git/error_test.go
+++ b/workhorse/internal/git/error_test.go
@@ -2,12 +2,14 @@ package git
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 	"gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb"
+	"gitlab.com/gitlab-org/labkit/correlation"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
@@ -16,17 +18,19 @@ import (
 )
 
 func TestHandleLimitErr(t *testing.T) {
+	const expectedCorrelationID = "abcdef01234"
+
 	testCases := []struct {
 		desc          string
-		errWriter     func(io.Writer) error
+		errWriter     func(io.Writer, string) error
 		expectedBytes []byte
 	}{
 		{
 			desc:      "upload pack",
 			errWriter: writeUploadPackError,
 			expectedBytes: bytes.Join([][]byte{
-				[]byte{'0', '0', '4', '7'},
-				[]byte("ERR GitLab is currently unable to handle this request due to load.\n"),
+				{'0', '0', '5', '8'},
+				[]byte("ERR GitLab is currently unable to handle this request due to load (ID abcdef01234).\n"),
 			}, []byte{}),
 		},
 		{
@@ -35,13 +39,15 @@ func TestHandleLimitErr(t *testing.T) {
 			expectedBytes: bytes.Join([][]byte{
 				{'0', '0', '2', '3', 1, '0', '0', '1', 'a'},
 				[]byte("unpack server is busy\n"),
-				{'0', '0', '0', '0', '0', '0', '4', '4', 2},
-				[]byte("GitLab is currently unable to handle this request due to load.\n"),
+				{'0', '0', '0', '0', '0', '0', '5', '5', 2},
+				[]byte("GitLab is currently unable to handle this request due to load (ID abcdef01234).\n"),
 				{'0', '0', '0', '0'},
 			}, []byte{}),
 		},
 	}
 
+	ctx := correlation.ContextWithCorrelation(context.Background(), expectedCorrelationID)
+
 	for _, tc := range testCases {
 		t.Run(tc.desc, func(t *testing.T) {
 			var body bytes.Buffer
@@ -49,7 +55,7 @@ func TestHandleLimitErr(t *testing.T) {
 				ErrorMessage: "concurrency queue wait time reached",
 				RetryAfter:   durationpb.New(0)})
 
-			handleLimitErr(fmt.Errorf("wrapped error: %w", err), &body, tc.errWriter)
+			handleLimitErr(fmt.Errorf("wrapped error: %w", err), &body, ctx, tc.errWriter)
 			require.Equal(t, tc.expectedBytes, body.Bytes())
 		})
 	}
@@ -57,12 +63,11 @@ func TestHandleLimitErr(t *testing.T) {
 	t.Run("non LimitError", func(t *testing.T) {
 		var body bytes.Buffer
 		err := status.Error(codes.Internal, "some internal error")
-		handleLimitErr(fmt.Errorf("wrapped error: %w", err), &body, writeUploadPackError)
-		require.Equal(t, []byte(nil), body.Bytes())
-
-		handleLimitErr(fmt.Errorf("wrapped error: %w", err), &body, writeReceivePackError)
-		require.Equal(t, []byte(nil), body.Bytes())
+		handleLimitErr(fmt.Errorf("wrapped error: %w", err), &body, ctx, writeUploadPackError)
+		require.Equal(t, []byte(nil), body.Bytes(), expectedCorrelationID)
 
+		handleLimitErr(fmt.Errorf("wrapped error: %w", err), &body, ctx, writeReceivePackError)
+		require.Equal(t, []byte(nil), body.Bytes(), expectedCorrelationID)
 	})
 }
 
diff --git a/workhorse/internal/git/git-http.go b/workhorse/internal/git/git-http.go
index de57399aa9936d2de2447f83f86f4e556bc03d4c..b1f2713c7c56a4f27cdbc6dc640f602d2cee1f98 100644
--- a/workhorse/internal/git/git-http.go
+++ b/workhorse/internal/git/git-http.go
@@ -46,7 +46,7 @@ func postRPCHandler(
 	name string,
 	handler func(*HTTPResponseWriter, *http.Request, *api.Response) (*gitalypb.PackfileNegotiationStatistics, error),
 	postFunc func(*api.API, *http.Request, *api.Response, *gitalypb.PackfileNegotiationStatistics),
-	errWriter func(io.Writer) error,
+	errWriter func(io.Writer, string) error,
 ) http.Handler {
 	return repoPreAuthorizeHandler(a, func(rw http.ResponseWriter, r *http.Request, ar *api.Response) {
 		cr := &countReadCloser{ReadCloser: r.Body}
@@ -60,7 +60,7 @@ func postRPCHandler(
 
 		stats, err := handler(w, r, ar)
 		if err != nil {
-			handleLimitErr(err, w, errWriter)
+			handleLimitErr(err, w, r.Context(), errWriter)
 			// If the handler, or handleLimitErr already wrote a response this WriteHeader call is a
 			// no-op. It never reaches net/http because GitHttpResponseWriter calls
 			// WriteHeader on its underlying ResponseWriter at most once.