diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
index 583573bdbfb04d932f15c5588385ede1417236eb..2d81159310dde356ea8d7ccb6e9b485e4e5ebb06 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs
@@ -68,6 +68,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
 
         private string? _requestId;
         private int _requestHeadersParsed;
+        // See MaxRequestHeaderCount, enforced during parsing and may be more relaxed to avoid connection faults.
+        protected int _eagerRequestHeadersParsedLimit;
 
         private long _responseBytesWritten;
 
@@ -112,6 +114,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
         public long? MaxRequestBodySize { get; set; }
         public MinDataRate? MinRequestBodyDataRate { get; set; }
         public bool AllowSynchronousIO { get; set; }
+        protected int RequestHeadersParsed => _requestHeadersParsed;
 
         /// <summary>
         /// The request id. <seealso cref="HttpContext.TraceIdentifier"/>
@@ -413,6 +416,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             Output?.Reset();
 
             _requestHeadersParsed = 0;
+            _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount;
 
             _responseBytesWritten = 0;
 
@@ -544,7 +548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
         private void IncrementRequestHeadersCount()
         {
             _requestHeadersParsed++;
-            if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
+            if (_requestHeadersParsed > _eagerRequestHeadersParsedLimit)
             {
                 KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
             }
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
index 2f54decb91b64dbcf0d838c487f5fa90640a3909..b197c7fccb28caac5ee02994674d6e482978700e 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
@@ -1019,6 +1019,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
 
             try
             {
+                _currentHeadersStream.TotalParsedHeaderSize = _totalParsedHeaderSize;
+
                 // This must be initialized before we offload the request or else we may start processing request body frames without it.
                 _currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength;
 
@@ -1279,8 +1281,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
 
             // https://tools.ietf.org/html/rfc7540#section-6.5.2
             // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field.";
-            _totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
-            if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
+            // We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit.
+            _totalParsedHeaderSize += name.Length + value.Length;
+            // Allow a 2x grace before aborting the connection. We'll check the size limit again later where we can send a 431.
+            if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2)
             {
                 throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR);
             }
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
index 685b25220e15bc76c75644c2cb9e2a6ca4c2c3bc..47812b0c9df497efa3de1455894ce3dc724539c9 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
@@ -31,6 +31,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
 
         private bool _decrementCalled;
 
+        public int TotalParsedHeaderSize { get; set; }
+
         public Pipe RequestBodyPipe { get; private set; } = default!;
 
         internal long DrainExpirationTicks { get; set; }
@@ -47,6 +49,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
             InputRemaining = null;
             RequestBodyStarted = false;
             DrainExpirationTicks = 0;
+            TotalParsedHeaderSize = 0;
+            // Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection.
+            _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2;
 
             _context = context;
 
@@ -208,6 +213,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
             // We don't need any of the parameters because we don't implement BeginRead to actually
             // do the reading from a pipeline, nor do we use endConnection to report connection-level errors.
             endConnection = !TryValidatePseudoHeaders();
+
+            // 431 if the headers are too large
+            if (TotalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize)
+            {
+                KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
+            }
+
+            // 431 if we received too many headers
+            if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
+            {
+                KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
+            }
+
             return true;
         }
 
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
index 0f862c69fe8bd2a1b1ca7c4bd147bc63b7695ba2..31fdab59d66251763d1227de005f6297adfbdab8 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
@@ -92,6 +92,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
             _requestHeaderParsingState = default;
             _parsedPseudoHeaderFields = default;
             _totalParsedHeaderSize = 0;
+            // Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection.
+            _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2;
             _isMethodConnect = false;
             _completionState = default;
             StreamTimeoutTicks = 0;
@@ -205,10 +207,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
 
         public override void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value, bool checkForNewlineChars)
         {
-            // https://tools.ietf.org/html/rfc7540#section-6.5.2
+            // https://httpwg.org/specs/rfc9114.html#rfc.section.4.2.2
             // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field.";
-            _totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
-            if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
+            // We don't include the 32 byte overhead hear so we can accept a little more than the advertised limit.
+            _totalParsedHeaderSize += name.Length + value.Length;
+            // Allow a 2x grace before aborting the stream. We'll check the size limit again later where we can send a 431.
+            if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize * 2)
             {
                 throw new Http3StreamErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
             }
@@ -754,6 +758,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
         protected override bool TryParseRequest(ReadResult result, out bool endConnection)
         {
             endConnection = !TryValidatePseudoHeaders();
+
+            // 431 if the headers are too large
+            if (_totalParsedHeaderSize > ServerOptions.Limits.MaxRequestHeadersTotalSize)
+            {
+                KestrelBadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
+            }
+
+            // 431 if we received too many headers
+            if (RequestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
+            {
+                KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
+            }
+
             return true;
         }
 
diff --git a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs
index bb3071ef4a99104c941f1dc800ff0afa421097e6..d1f2fa29705bc8184dde2f838f389f2d6a8c8d0a 100644
--- a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs
+++ b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs
@@ -137,6 +137,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         {
             const string headerLines = "Header-1: value1\r\nHeader-2: value2\r\n";
             _serviceContext.ServerOptions.Limits.MaxRequestHeaderCount = 1;
+            _http1Connection.Initialize(_http1ConnectionContext);
 
             await _application.Output.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n"));
             var readableBuffer = (await _transport.Input.ReadAsync()).Buffer;
diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
index af0a22c3ece6a667a0eb4c80c3e0e8f048684376..9a98b849280c2140f75adb037c2c7d4d8c166fa5 100644
--- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
+++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
@@ -590,7 +590,7 @@ namespace Microsoft.AspNetCore.Testing
 
     internal class Http3RequestHeaderHandler
     {
-        public readonly byte[] HeaderEncodingBuffer = new byte[64 * 1024];
+        public readonly byte[] HeaderEncodingBuffer = new byte[96 * 1024];
         public readonly QPackDecoder QpackDecoder = new QPackDecoder(8192);
         public readonly Dictionary<string, string> DecodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
     }
@@ -639,9 +639,8 @@ namespace Microsoft.AspNetCore.Testing
             var done = QPackHeaderWriter.BeginEncode(headers, buffer.Span, ref headersTotalSize, out var length);
             if (!done)
             {
-                throw new InvalidOperationException("Headers not sent.");
+                throw new InvalidOperationException("The headers are too large.");
             }
-
             await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream);
         }
 
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
index 6a9656d9f730a439104309258ebc17e5082182dc..e83b91a349f7f167aeeb561201b4851624b32a0c 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
@@ -2712,7 +2712,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         [Fact]
         public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
         {
-            // > 32kb
+            // > 32kb * 2 to exceed graceful handling limit
             var headers = new[]
             {
                 new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
@@ -2726,6 +2726,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                 new KeyValuePair<string, string>("f", _4kHeaderValue),
                 new KeyValuePair<string, string>("g", _4kHeaderValue),
                 new KeyValuePair<string, string>("h", _4kHeaderValue),
+                new KeyValuePair<string, string>("i", _4kHeaderValue),
+                new KeyValuePair<string, string>("j", _4kHeaderValue),
+                new KeyValuePair<string, string>("k", _4kHeaderValue),
+                new KeyValuePair<string, string>("l", _4kHeaderValue),
+                new KeyValuePair<string, string>("m", _4kHeaderValue),
+                new KeyValuePair<string, string>("n", _4kHeaderValue),
+                new KeyValuePair<string, string>("o", _4kHeaderValue),
+                new KeyValuePair<string, string>("p", _4kHeaderValue),
             };
 
             return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize);
@@ -2734,7 +2742,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         [Fact]
         public Task HEADERS_Received_TooManyHeaders_ConnectionError()
         {
-            // > MaxRequestHeaderCount (100)
+            // > MaxRequestHeaderCount (100) * 2 to exceed graceful handling limit
             var headers = new List<KeyValuePair<string, string>>();
             headers.AddRange(new[]
             {
@@ -2742,7 +2750,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                 new KeyValuePair<string, string>(HeaderNames.Path, "/"),
                 new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
             });
-            for (var i = 0; i < 100; i++)
+            for (var i = 0; i < 200; i++)
             {
                 headers.Add(new KeyValuePair<string, string>(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture)));
             }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index 0fe7184e2e9f044609a79f440027f5705bd31e65..97f0d80506e993e01b6189a211b1e07074bb337f 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -4,6 +4,7 @@
 using System;
 using System.Buffers;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net.Http;
@@ -740,6 +741,77 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
         }
 
+        [Fact]
+        public async Task HEADERS_Received_MaxRequestHeadersTotalSize_431()
+        {
+            // > 32kb
+            var headers = new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+                new KeyValuePair<string, string>("a", _4kHeaderValue),
+                new KeyValuePair<string, string>("b", _4kHeaderValue),
+                new KeyValuePair<string, string>("c", _4kHeaderValue),
+                new KeyValuePair<string, string>("d", _4kHeaderValue),
+                new KeyValuePair<string, string>("e", _4kHeaderValue),
+                new KeyValuePair<string, string>("f", _4kHeaderValue),
+                new KeyValuePair<string, string>("g", _4kHeaderValue),
+                new KeyValuePair<string, string>("h", _4kHeaderValue),
+            };
+            await InitializeConnectionAsync(_notImplementedApp);
+
+            await StartStreamAsync(1, headers, endStream: true);
+
+            var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 40,
+                withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+                withStreamId: 1);
+
+            await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+            _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
+
+            Assert.Equal(3, _decodedHeaders.Count);
+            Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+            Assert.Equal("431", _decodedHeaders[HeaderNames.Status]);
+            Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+        }
+
+        [Fact]
+        public async Task HEADERS_Received_MaxRequestHeaderCount_431()
+        {
+            // > 100 headers
+            var headers = new List<KeyValuePair<string, string>>()
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+            };
+            for (var i = 0; i < 101; i++)
+            {
+                var text = i.ToString(CultureInfo.InvariantCulture);
+                headers.Add(new KeyValuePair<string, string>(text, text));
+            }
+            await InitializeConnectionAsync(_notImplementedApp);
+
+            await StartStreamAsync(1, headers, endStream: true);
+
+            var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 40,
+                withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+                withStreamId: 1);
+
+            await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+            _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: true, handler: this);
+
+            Assert.Equal(3, _decodedHeaders.Count);
+            Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+            Assert.Equal("431", _decodedHeaders[HeaderNames.Status]);
+            Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
+        }
+
         [Fact]
         public async Task ContentLength_Received_SingleDataFrame_Verified()
         {
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
index e0d92c63c8bbc5d0260da9039378216e20146e1f..4fc1e1e0cc1ff738f2e8a8ae5ed2d1ee976b23d2 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
@@ -143,6 +143,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         protected readonly TaskCompletionSource _closedStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
         protected readonly RequestDelegate _noopApplication;
+        protected readonly RequestDelegate _notImplementedApp;
         protected readonly RequestDelegate _readHeadersApplication;
         protected readonly RequestDelegate _readTrailersApplication;
         protected readonly RequestDelegate _bufferingApplication;
@@ -193,6 +194,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             });
 
             _noopApplication = context => Task.CompletedTask;
+            _notImplementedApp = _ => throw new NotImplementedException();
 
             _readHeadersApplication = context =>
             {
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
index 6a82f44102c8688f1e48d19f808513e42bdce411..2fc056b6bdd24a7715222541cd429a97bb130bea 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
@@ -2319,7 +2319,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         }
 
         [Fact]
-        public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
+        public async Task HEADERS_Received_HeaderBlockOverLimit_431()
         {
             // > 32kb
             var headers = new[]
@@ -2336,12 +2336,51 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                 new KeyValuePair<string, string>("g", _4kHeaderValue),
                 new KeyValuePair<string, string>("h", _4kHeaderValue),
             };
+            var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication);
+            await requestStream.SendHeadersAsync(headers, endStream: true);
+
+            var receivedHeaders = await requestStream.ExpectHeadersAsync();
+
+            await requestStream.ExpectReceiveEndOfStream();
+
+            Assert.Equal(3, receivedHeaders.Count);
+            Assert.Contains("date", receivedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+            Assert.Equal("431", receivedHeaders[HeaderNames.Status]);
+            Assert.Equal("0", receivedHeaders[HeaderNames.ContentLength]);
+        }
+
+        [Fact]
+        public Task HEADERS_Received_HeaderBlockOverLimitx2_ConnectionError()
+        {
+            // > 32kb * 2 to exceed graceful handling limit
+            var headers = new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+                new KeyValuePair<string, string>("a", _4kHeaderValue),
+                new KeyValuePair<string, string>("b", _4kHeaderValue),
+                new KeyValuePair<string, string>("c", _4kHeaderValue),
+                new KeyValuePair<string, string>("d", _4kHeaderValue),
+                new KeyValuePair<string, string>("e", _4kHeaderValue),
+                new KeyValuePair<string, string>("f", _4kHeaderValue),
+                new KeyValuePair<string, string>("g", _4kHeaderValue),
+                new KeyValuePair<string, string>("h", _4kHeaderValue),
+                new KeyValuePair<string, string>("i", _4kHeaderValue),
+                new KeyValuePair<string, string>("j", _4kHeaderValue),
+                new KeyValuePair<string, string>("k", _4kHeaderValue),
+                new KeyValuePair<string, string>("l", _4kHeaderValue),
+                new KeyValuePair<string, string>("m", _4kHeaderValue),
+                new KeyValuePair<string, string>("n", _4kHeaderValue),
+                new KeyValuePair<string, string>("o", _4kHeaderValue),
+                new KeyValuePair<string, string>("p", _4kHeaderValue),
+            };
 
             return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
         }
 
         [Fact]
-        public Task HEADERS_Received_TooManyHeaders_ConnectionError()
+        public async Task HEADERS_Received_TooManyHeaders_431()
         {
             // > MaxRequestHeaderCount (100)
             var headers = new List<KeyValuePair<string, string>>();
@@ -2356,6 +2395,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
                 headers.Add(new KeyValuePair<string, string>(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture)));
             }
 
+            var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_notImplementedApp);
+            await requestStream.SendHeadersAsync(headers, endStream: true);
+
+            var receivedHeaders = await requestStream.ExpectHeadersAsync();
+
+            await requestStream.ExpectReceiveEndOfStream();
+
+            Assert.Equal(3, receivedHeaders.Count);
+            Assert.Contains("date", receivedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+            Assert.Equal("431", receivedHeaders[HeaderNames.Status]);
+            Assert.Equal("0", receivedHeaders[HeaderNames.ContentLength]);
+        }
+
+        [Fact]
+        public Task HEADERS_Received_TooManyHeadersx2_ConnectionError()
+        {
+            // > MaxRequestHeaderCount (100) * 2 to exceed graceful handling limit
+            var headers = new List<KeyValuePair<string, string>>();
+            headers.AddRange(new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+            });
+            for (var i = 0; i < 200; i++)
+            {
+                headers.Add(new KeyValuePair<string, string>(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture)));
+            }
+
             return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_TooManyHeaders);
         }
 
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
index be046dc261416fcf3cbed23a484d4f612fbc2bde..b0ce32f5ef0ebfa95e717fecb4ff4adbd493880d 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
@@ -50,6 +50,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         internal readonly Mock<ITimeoutHandler> _mockTimeoutHandler = new Mock<ITimeoutHandler>();
 
         protected readonly RequestDelegate _noopApplication;
+        protected readonly RequestDelegate _notImplementedApp;
         protected readonly RequestDelegate _echoApplication;
         protected readonly RequestDelegate _readRateApplication;
         protected readonly RequestDelegate _echoMethod;
@@ -80,6 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         public Http3TestBase()
         {
             _noopApplication = context => Task.CompletedTask;
+            _notImplementedApp = _ => throw new NotImplementedException();
 
             _echoApplication = async context =>
             {
diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs
index be2eaa97e93ccfb2a4668403cac623d93c2a01e7..ee85ae94b7a9ceb51f13801267458c5cde5e2158 100644
--- a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs
+++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs
@@ -1366,10 +1366,10 @@ namespace Interop.FunctionalTests
             {
                 request.Headers.Add("header" + i, oneKbString + i);
             }
-            // Kestrel closes the connection rather than sending the recommended 431 response. https://github.com/dotnet/aspnetcore/issues/17861
-            await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(request)).DefaultTimeout();
-
+            var response = await client.SendAsync(request).DefaultTimeout();
             await host.StopAsync().DefaultTimeout();
+
+            Assert.Equal(HttpStatusCode.RequestHeaderFieldsTooLarge, response.StatusCode);
         }
 
         [Theory]