diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs
index 07d723e4da871debda50fb83b4fc327cf3b586bf..ea0bb16a043224e43d6a202274c47349983b8f63 100644
--- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs
+++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs
@@ -486,8 +486,15 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
                 var response = await deploymentResult.HttpClient.GetAsync("/Abort").TimeoutAfter(TimeoutExtensions.DefaultTimeoutValue);
 
                 Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode);
+
+#if NEWSHIM_FUNCTIONALS
+                // In-proc SocketConnection isn't used and there's no abort
                 // 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response
                 Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync());
+#else
+                // 0x80072efe ERROR_INTERNET_CONNECTION_ABORTED The connection with the server was terminated abnormally
+                Assert.Contains("0x80072efe", await response.Content.ReadAsStringAsync());
+#endif
             }
             catch (HttpRequestException)
             {
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs
index ad1ffff50e52aa9becc9d5a57c9afecd2500d0ef..2a5927b901e61fb09cdbadfc1650e908890e86d5 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs
@@ -93,7 +93,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             _http1Output.Dispose();
         }
 
-        public void OnInputOrOutputCompleted()
+        void IRequestProcessor.OnInputOrOutputCompleted()
+        {
+            // Closed gracefully.
+            _http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!);
+            CancelRequestAbortedToken();
+        }
+
+        void IHttpOutputAborter.OnInputOrOutputCompleted()
         {
             _http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
             CancelRequestAbortedToken();
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs
index 5a3051ae56c8681839352df922571b227c436309..b9711909eb76fe64f5b283541cb79afcc66dc81a 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs
@@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             // so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400
             // response is written after observing the unexpected end of request content instead of just
             // closing the connection without a response as expected.
-            _context.OnInputOrOutputCompleted();
+            ((IHttpOutputAborter)_context).OnInputOrOutputCompleted();
 
             KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent);
         }
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
index b197c7fccb28caac5ee02994674d6e482978700e..ac3e9d166d4d9e54f0ccbbdd9c4ec9b3c8d2a194 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
@@ -150,7 +150,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
         public void OnInputOrOutputCompleted()
         {
             TryClose();
-            _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
+            var useException = _context.ServiceContext.ServerOptions.FinOnError || _clientActiveStreamCount != 0;
+            _frameWriter.Abort(useException ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!);
         }
 
         public void Abort(ConnectionAbortedException ex)
diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
index cf930c744cbb60bd83c8604c1d464b5d65f3178a..f732302c251fe0ddad711544ebf93faecb666cb6 100644
--- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
+++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs
@@ -29,9 +29,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
     /// </summary>
     public class KestrelServerOptions
     {
+        private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
+        private static readonly bool _finOnError;
+
+        static KestrelServerOptions()
+        {
+            AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
+        }
+
         // internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
         internal static readonly Func<string, Encoding?> DefaultHeaderEncodingSelector = _ => null;
 
+        // Opt-out flag for back compat. Remove in 9.0 (or make public).
+        internal bool FinOnError { get; set; } = _finOnError;
+
         private Func<string, Encoding?> _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector;
 
         private Func<string, Encoding?> _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector;
diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs
index 60966046a3a36a06aef433707f055853987cc4e8..260657cff99b4aafda3debd13cd3b525bd3856b4 100644
--- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs
@@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
 
         void ConnectionWriteFin(string connectionId, string reason);
 
+        void ConnectionWriteRst(string connectionId, string reason);
+
         void ConnectionWrite(string connectionId, int count);
 
         void ConnectionWriteCallback(string connectionId, int status);
diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs
index fdfc852ba243178b2a4598cbb34596c53b1c97f9..ca163d6d2aff4df77a71bd9320c25ecdf7e13d55 100644
--- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs
@@ -6,6 +6,8 @@ using System.Buffers;
 using System.IO;
 using System.IO.Pipelines;
 using System.Net;
+using System.Net.Sockets;
+using System.Reflection.Metadata;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
@@ -28,6 +30,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
         private readonly IDuplexPipe _originalTransport;
         private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource();
 
+        private readonly bool _finOnError;
+
         private volatile ConnectionAbortedException _abortReason;
 
         private MemoryHandle _bufferHandle;
@@ -43,9 +47,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
                                PipeOptions inputOptions = null,
                                PipeOptions outputOptions = null,
                                long? maxReadBufferSize = null,
-                               long? maxWriteBufferSize = null)
+                               long? maxWriteBufferSize = null,
+                               bool finOnError = false)
         {
             _socket = socket;
+            _finOnError = finOnError;
 
             LocalEndPoint = localEndPoint;
             RemoteEndPoint = remoteEndPoint;
@@ -124,6 +130,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
                 {
                     inputError ??= _abortReason ?? new ConnectionAbortedException("The libuv transport's send loop completed gracefully.");
 
+                    if (!_finOnError && _abortReason is not null)
+                    {
+                        // When shutdown isn't clean (note that we're using _abortReason, rather than inputError, to exclude that case),
+                        // we set the DontLinger socket option to cause libuv to send a RST and release any buffered response data.
+                        SetDontLingerOption(_socket);
+                    }
+
                     // Now, complete the input so that no more reads can happen
                     Input.Complete(inputError);
                     Output.Complete(outputError);
@@ -132,8 +145,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
                     // on the stream handle
                     Input.CancelPendingFlush();
 
-                    // Send a FIN
-                    Log.ConnectionWriteFin(ConnectionId, inputError.Message);
+                    if (!_finOnError && _abortReason is not null)
+                    {
+                        // Send a RST
+                        Log.ConnectionWriteRst(ConnectionId, inputError.Message);
+                    }
+                    else
+                    {
+                        // Send a FIN
+                        Log.ConnectionWriteFin(ConnectionId, inputError.Message);
+                    }
 
                     // We're done with the socket now
                     _socket.Dispose();
@@ -150,6 +171,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
             }
         }
 
+        /// <remarks>
+        /// This should be called on <see cref="_socket"/> before it is disposed.
+        /// Both <see cref="Abort"/> and <see cref="StartCore"/> call dispose but, rather than predict
+        /// which will do so first (which varies), we make this method idempotent and call it in both.
+        /// </remarks>
+        private static void SetDontLingerOption(UvStreamHandle socket)
+        {
+            if (!socket.IsClosed && !socket.IsInvalid)
+            {
+                var libuv = socket.Libuv;
+                var pSocket = IntPtr.Zero;
+                libuv.uv_fileno(socket, ref pSocket);
+
+                // libuv doesn't expose setsockopt, so we take advantage of the fact that
+                // Socket already has a PAL
+                using var managedHandle = new SafeSocketHandle(pSocket, ownsHandle: false);
+                using var managedSocket = new Socket(managedHandle);
+                managedSocket.LingerState = new LingerOption(enable: true, seconds: 0);
+            }
+        }
+
         public override void Abort(ConnectionAbortedException abortReason)
         {
             _abortReason = abortReason;
@@ -157,8 +199,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
             // Cancel WriteOutputAsync loop after setting _abortReason.
             Output.CancelPendingRead();
 
-            // This cancels any pending I/O.
-            Thread.Post(s => s.Dispose(), _socket);
+            Thread.Post(static (self) =>
+            {
+                if (!self._finOnError)
+                {
+                    SetDontLingerOption(self._socket);
+                }
+
+                // This cancels any pending I/O.
+                self._socket.Dispose();
+            }, this);
         }
 
         public override async ValueTask DisposeAsync()
diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs
index bdfcc2e65b58c9c15faeee676999c5d13bb3faf4..f03f8e603097ec36940e2daebb405fa337c61d17 100644
--- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Net;
 using System.Threading;
@@ -134,9 +135,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
         {
             // TODO: Move thread management to LibuvTransportFactory
             // TODO: Split endpoint management from thread management
+
+            // When `FinOnError` is false (the default), we need to be able to forcibly abort connections.
+            // On Windows, libuv 1.10.0 will call `shutdown`, preventing forcible abort, on any socket
+            // not flagged as `UV_HANDLE_SHARED_TCP_SOCKET`.  The only way we've found to cause socket
+            // to be flagged as `UV_HANDLE_SHARED_TCP_SOCKET` is to share it across a named pipe (which
+            // must, itself, be flagged `ipc`), which naturally happens when a `ListenerPrimary` dispatches
+            // a connection to a `ListenerSecondary`.  Therefore, in scenarios where this is required, we
+            // tell the `ListenerPrimary` to dispatch *all* connections to secondary and create an
+            // additional `ListenerSecondary` to replace the lost capacity.
+            var dispatchAllToSecondary = Libuv.IsWindows && !TransportContext.Options.FinOnError;
+
 #pragma warning disable CS0618
-            for (var index = 0; index < TransportOptions.ThreadCount; index++)
+            var threadCount = dispatchAllToSecondary
+                ? TransportOptions.ThreadCount + 1
+                : TransportOptions.ThreadCount;
 #pragma warning restore CS0618
+
+            for (var index = 0; index < threadCount; index++)
             {
                 Threads.Add(new LibuvThread(Libuv, TransportContext));
             }
@@ -148,10 +164,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
 
             try
             {
-#pragma warning disable CS0618
-                if (TransportOptions.ThreadCount == 1)
-#pragma warning restore CS0618
+                if (threadCount == 1)
                 {
+                    Debug.Assert(!dispatchAllToSecondary, "Should have taken the primary/secondary code path");
+
                     var listener = new Listener(TransportContext);
                     _listeners.Add(listener);
                     await listener.StartAsync(EndPoint, Threads[0]).ConfigureAwait(false);
@@ -162,7 +178,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
                     var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
                     var pipeMessage = Guid.NewGuid().ToByteArray();
 
-                    var listenerPrimary = new ListenerPrimary(TransportContext);
+                    var listenerPrimary = new ListenerPrimary(TransportContext, dispatchAllToSecondary);
                     _listeners.Add(listenerPrimary);
                     await listenerPrimary.StartAsync(pipeName, pipeMessage, EndPoint, Threads[0]).ConfigureAwait(false);
                     EndPoint = listenerPrimary.EndPoint;
diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs
index f4e4d64aafff8b844f0ae1a1f19884c673fef7b7..c360efc7bf93f11809c6a258c83a41ee00f76871 100644
--- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs
@@ -27,6 +27,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
         [LoggerMessage(7, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending FIN because: ""{Reason}""", EventName = nameof(ConnectionWriteFin))]
         public partial void ConnectionWriteFin(string connectionId, string reason);
 
+        [LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = nameof(ConnectionWriteRst))]
+        public partial void ConnectionWriteRst(string connectionId, string reason);
+
         public void ConnectionWrite(string connectionId, int count)
         {
             // Don't log for now since this could be *too* verbose.
diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs
index 7ad346f6ea59064a90d98bdd48d5111c32460fd9..8ab2f5f34503fdcd02c0a888cc6d9b76be61bc23 100644
--- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs
@@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
 
                 var options = TransportContext.Options;
 #pragma warning disable CS0618
-                var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize);
+                var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize, options.FinOnError);
 #pragma warning restore CS0618
                 connection.Start();
 
diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs
index 8a899c91c44ebe3836f1a4710dcf290715d5d9cc..18ae24af5adb6dc51af6d1c68bc81340e3ce6b71 100644
--- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Net;
 using System.Runtime.InteropServices;
@@ -22,6 +23,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
         private readonly List<UvPipeHandle> _dispatchPipes = new List<UvPipeHandle>();
         // The list of pipes we've created but may not be part of _dispatchPipes
         private readonly List<UvPipeHandle> _createdPipes = new List<UvPipeHandle>();
+
+        // If true, dispatch all connections to _dispatchPipes - don't process any in the primary
+        private readonly bool _dispatchAll;
+
         private int _dispatchIndex;
         private string _pipeName;
         private byte[] _pipeMessage;
@@ -32,8 +37,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
         // but it has no other functional significance
         private readonly ArraySegment<ArraySegment<byte>> _dummyMessage = new ArraySegment<ArraySegment<byte>>(new[] { new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 }) });
 
-        public ListenerPrimary(LibuvTransportContext transportContext) : base(transportContext)
+        public ListenerPrimary(LibuvTransportContext transportContext, bool dispatchAll) : base(transportContext)
         {
+            _dispatchAll = dispatchAll;
         }
 
         /// <summary>
@@ -107,9 +113,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
 
         protected override void DispatchConnection(UvStreamHandle socket)
         {
-            var index = _dispatchIndex++ % (_dispatchPipes.Count + 1);
+            var modulus = _dispatchAll ? _dispatchPipes.Count : (_dispatchPipes.Count + 1);
+            if (modulus == 0)
+            {
+                if (_createdPipes.Count == 0)
+                {
+#pragma warning disable CS0618 // Type or member is obsolete
+                    Log.LogError(0, $"Connection received before listeners were initialized - see https://aka.ms/dotnet/aspnet/finonerror for possible mitigations");
+#pragma warning restore CS0618 // Type or member is obsolete
+                }
+                else
+                {
+                    Log.LogError(0, "Unable to process connection since listeners failed to initialize - see https://aka.ms/dotnet/aspnet/finonerror for possible mitigations");
+                }
+
+                return;
+            }
+
+            var index = _dispatchIndex++ % modulus;
             if (index == _dispatchPipes.Count)
             {
+                Debug.Assert(!_dispatchAll, "Should have dispatched to a secondary listener");
                 base.DispatchConnection(socket);
             }
             else
diff --git a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs
index 142a2f99c4cefe7c0c083c47f8cb60420a28b768..9e58ac1a2d7ad7c0e988f0deea5b869e2cbf21ff 100644
--- a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs
@@ -12,6 +12,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv
     [Obsolete("The libuv transport is obsolete and will be removed in a future release. See https://aka.ms/libuvtransport for details.", error: false)] // Remove after .NET 6.
     public class LibuvTransportOptions
     {
+        private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
+        private static readonly bool _finOnError;
+
+        static LibuvTransportOptions()
+        {
+            AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
+        }
+
+        // Opt-out flag for back compat. Remove in 7.0.
+        internal bool FinOnError { get; set; } = _finOnError;
+
         /// <summary>
         /// The number of libuv I/O threads used to process requests.
         /// </summary>
diff --git a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs
index 82d04cd1ac14b1cf0e1da96ba94db1c456a236a1..58d57451490e295d2e7c7ba39fe2ec3648486297 100644
--- a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs
+++ b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs
@@ -5,11 +5,14 @@ using System;
 using System.IO;
 using System.Linq;
 using System.Net;
+using System.Net.Sockets;
+using System.Text;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal;
 using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking;
 using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers;
 using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Xunit;
 
@@ -33,7 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
             // Start primary listener
             var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary);
             await libuvThreadPrimary.StartAsync();
-            var listenerPrimary = new ListenerPrimary(transportContextPrimary);
+            var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false);
             await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary);
             var address = GetUri(listenerPrimary.EndPoint);
 
@@ -50,6 +53,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
             }
 
             var listenerCount = listenerPrimary.UvPipeCount;
+            Assert.Equal(0, listenerCount);
+
             // Add secondary listener
             var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary);
             await libuvThreadSecondary.StartAsync();
@@ -85,6 +90,74 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
             await listenerPrimary.DisposeAsync();
             await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5));
         }
+        [Theory]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(3)]
+        public async Task ConnectionsGetRoundRobinedToSecondaryListeners_DispatchAll(int secondaryCount)
+        {
+            var libuv = new LibuvFunctions();
+
+            var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
+
+            var transportContextPrimary = new TestLibuvTransportContext();
+            var transportContextSecondary = new TestLibuvTransportContext();
+
+            var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
+            var pipeMessage = Guid.NewGuid().ToByteArray();
+
+            // Start primary listener
+            var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary);
+            await libuvThreadPrimary.StartAsync();
+            var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true);
+            await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary);
+            var address = GetUri(listenerPrimary.EndPoint);
+
+            Assert.Equal(0, listenerPrimary.UvPipeCount);
+
+            // Add secondary listeners
+            var listenerSecondaries = new ListenerSecondary[secondaryCount];
+            for (int i = 0; i < secondaryCount; i++)
+            {
+                var libuvThread = new LibuvThread(libuv, transportContextSecondary);
+                await libuvThread.StartAsync();
+                var listener = new ListenerSecondary(transportContextSecondary);
+                await listener.StartAsync(pipeName, pipeMessage, endpoint, libuvThread);
+                listenerSecondaries[i] = listener;
+            }
+
+            var maxWait = Task.Delay(TestConstants.DefaultTimeout);
+            // wait for ListenerPrimary.ReadCallback to add the secondary pipe
+            while (listenerPrimary.UvPipeCount < secondaryCount)
+            {
+                var completed = await Task.WhenAny(maxWait, Task.Delay(100));
+                if (ReferenceEquals(completed, maxWait))
+                {
+                    throw new TimeoutException("Timed out waiting for secondary listener to become available");
+                }
+            }
+
+            // Check that the secondaries are visited in order and that it wraps
+            // around without hitting the primary
+            for (int i = 0; i < secondaryCount + 1; i++)
+            {
+                var expectedTask = listenerSecondaries[i % secondaryCount].AcceptAsync().AsTask();
+
+                using var socket = await HttpClientSlim.GetSocket(address);
+
+                await using var connection = await expectedTask.DefaultTimeout();
+            }
+
+            foreach (var listenerSecondary in listenerSecondaries)
+            {
+                var libuvThread = listenerSecondary.Thread;
+                await listenerSecondary.DisposeAsync();
+                await libuvThread.StopAsync(TimeSpan.FromSeconds(5));
+            }
+
+            await listenerPrimary.DisposeAsync();
+            await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5));
+        }
 
         // https://github.com/aspnet/KestrelHttpServer/issues/1182
         [Fact]
@@ -103,7 +176,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
             // Start primary listener
             var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary);
             await libuvThreadPrimary.StartAsync();
-            var listenerPrimary = new ListenerPrimary(transportContextPrimary);
+            var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false);
             await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary);
             var address = GetUri(listenerPrimary.EndPoint);
 
@@ -181,7 +254,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
             Assert.Equal(LogLevel.Debug, logMessage.LogLevel);
         }
 
-
         [Fact]
         public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored()
         {
@@ -199,7 +271,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
             // Start primary listener
             var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary);
             await libuvThreadPrimary.StartAsync();
-            var listenerPrimary = new ListenerPrimary(transportContextPrimary);
+            var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false);
             await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary);
             var address = GetUri(listenerPrimary.EndPoint);
 
@@ -235,6 +307,108 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
             Assert.Contains("Bad data", errorMessage.Exception.ToString());
         }
 
+        [Fact]
+        public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored_DispatchAllNoneRemaining()
+        {
+            var libuv = new LibuvFunctions();
+            var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
+
+            var logger = new TestApplicationErrorLogger();
+
+            var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) };
+            var transportContextSecondary = new TestLibuvTransportContext();
+
+            var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
+            var pipeMessage = Guid.NewGuid().ToByteArray();
+
+            // Start primary listener
+            var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary);
+            await libuvThreadPrimary.StartAsync();
+            var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true);
+            await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary);
+            var address = GetUri(listenerPrimary.EndPoint);
+
+            // Add secondary listener with wrong pipe message
+            var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary);
+            await libuvThreadSecondary.StartAsync();
+            var listenerSecondary = new ListenerSecondary(transportContextSecondary);
+            await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), endpoint, libuvThreadSecondary);
+
+            // Wait up to 10 seconds for error to be logged
+            for (var i = 0; i < 10 && logger.TotalErrorsLogged == 0; i++)
+            {
+                await Task.Delay(100);
+            }
+
+            Assert.Equal(1, logger.TotalErrorsLogged);
+
+            var badDataMessage = Assert.Single(logger.Messages.Where(m => m.LogLevel == LogLevel.Error));
+            Assert.IsType<IOException>(badDataMessage.Exception);
+            Assert.Contains("Bad data", badDataMessage.Exception.ToString());
+
+            using var socket = await HttpClientSlim.GetSocket(address);
+
+            var _ = listenerPrimary.AcceptAsync();
+
+            // Wait up to 10 seconds for error to be logged
+            for (var i = 0; i < 10 && logger.TotalErrorsLogged <= 1; i++)
+            {
+                await Task.Delay(100);
+            }
+
+            var noSecondariesMessage = logger.Messages.Last(m => m.LogLevel == LogLevel.Error);
+            Assert.Null(noSecondariesMessage.Exception);
+            Assert.Contains("listeners failed to initialize", noSecondariesMessage.Message);
+
+            Assert.Null(libuvThreadPrimary.FatalError);
+
+            await listenerSecondary.DisposeAsync();
+            await libuvThreadSecondary.StopAsync(TimeSpan.FromSeconds(5));
+
+            await listenerPrimary.DisposeAsync();
+            await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5));
+        }
+
+        [Fact]
+        public async Task DispatchAllConnectionBeforeSecondaries()
+        {
+            var libuv = new LibuvFunctions();
+            var endpoint = new IPEndPoint(IPAddress.Loopback, 0);
+
+            var logger = new TestApplicationErrorLogger();
+
+            var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) };
+            var transportContextSecondary = new TestLibuvTransportContext();
+
+            var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n");
+            var pipeMessage = Guid.NewGuid().ToByteArray();
+
+            // Start primary listener
+            var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary);
+            await libuvThreadPrimary.StartAsync();
+            var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true);
+            await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary);
+            var address = GetUri(listenerPrimary.EndPoint);
+
+            using var socket = await HttpClientSlim.GetSocket(address);
+
+            var _ = listenerPrimary.AcceptAsync();
+
+            // Wait up to 10 seconds for error to be logged
+            for (var i = 0; i < 10 && logger.TotalErrorsLogged <= 1; i++)
+            {
+                await Task.Delay(100);
+            }
+
+            var noSecondariesMessage = logger.Messages.Last(m => m.LogLevel == LogLevel.Error);
+            Assert.Null(noSecondariesMessage.Exception);
+            Assert.Contains("before listeners", noSecondariesMessage.Message);
+
+            Assert.Null(libuvThreadPrimary.FatalError);
+
+            await listenerPrimary.DisposeAsync();
+            await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5));
+        }
 
         private static async Task AssertRoundRobin(Uri address, ListenerPrimary listenerPrimary, ListenerSecondary listenerSecondary, ListenerContext currentListener, Task<LibuvConnection> expected = null, int connections = 4)
         {
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs
index f5e5c49637865cebe419c7f9697507704453a53e..cdd82f481e8f243d4fdc36f86f8b21f1d971b726 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs
@@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
         void ConnectionReadFin(SocketConnection connection);
 
         void ConnectionWriteFin(SocketConnection connection, string reason);
+        void ConnectionWriteRst(SocketConnection connection, string reason);
 
         void ConnectionError(SocketConnection connection, Exception ex);
 
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs
index f43c2e1454f1462aaaebb4e8505e6ad3c003969d..c7f58a128a82586bba1545571cc7f683d1509927 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
 {
+
     internal sealed partial class SocketConnection : TransportConnection
     {
         private static readonly int MinAllocBufferSize = PinnedBlockMemoryPool.BlockSize / 2;
@@ -30,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
         private readonly TaskCompletionSource _waitForConnectionClosedTcs = new TaskCompletionSource();
         private bool _connectionClosed;
         private readonly bool _waitForData;
+        private readonly bool _finOnError;
 
         internal SocketConnection(Socket socket,
                                   MemoryPool<byte> memoryPool,
@@ -38,7 +40,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
                                   SocketSenderPool socketSenderPool,
                                   PipeOptions inputOptions,
                                   PipeOptions outputOptions,
-                                  bool waitForData = true)
+                                  bool waitForData = true,
+                                  bool finOnError = false)
         {
             Debug.Assert(socket != null);
             Debug.Assert(memoryPool != null);
@@ -49,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
             _trace = trace;
             _waitForData = waitForData;
             _socketSenderPool = socketSenderPool;
+            _finOnError = finOnError;
 
             LocalEndPoint = _socket.LocalEndPoint;
             RemoteEndPoint = _socket.RemoteEndPoint;
@@ -329,11 +333,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
                 // ever observe the nondescript ConnectionAbortedException except for connection middleware attempting
                 // to half close the connection which is currently unsupported.
                 _shutdownReason = shutdownReason ?? new ConnectionAbortedException("The Socket transport's send loop completed gracefully.");
+
+	            // NB: not _shutdownReason since we don't want to do this on graceful completion
+	            if (!_finOnError && shutdownReason is not null)
+	            {
+                    _trace.ConnectionWriteRst(this, shutdownReason.Message);
+
+                    // This forces an abortive close with linger time 0 (and implies Dispose)
+                    _socket.Close(timeout: 0);
+	                return;
+	            }
+
                 _trace.ConnectionWriteFin(this, _shutdownReason.Message);
 
                 try
                 {
-                    // Try to gracefully close the socket even for aborts to match libuv behavior.
                     _socket.Shutdown(SocketShutdown.Both);
                 }
                 catch
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs
index 1a48fa189a15526118c9a569acb64e4298c98526..f4108eae678a651fc44b73b033b1fd5952c519b6 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs
@@ -43,6 +43,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
             }
         }
 
+        [LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = "ConnectionWriteRst", SkipEnabledCheck = true)]
+        private static partial void ConnectionWriteRst(ILogger logger, string connectionId, string reason);
+
+        public void ConnectionWriteRst(SocketConnection connection, string reason)
+        {
+            if (_logger.IsEnabled(LogLevel.Debug))
+            {
+                ConnectionWriteRst(_logger, connection.ConnectionId, reason);
+            }
+        }
+
         public void ConnectionWrite(SocketConnection connection, int count)
         {
             // Don't log for now since this could be *too* verbose.
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs
index 71f3cdc0fc0cc6544059863505d7a1024a9061ba..2635af9792d7e9e4a683aad7b1721241e87c4f46 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs
@@ -104,7 +104,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
                 setting.SocketSenderPool,
                 setting.InputOptions,
                 setting.OutputOptions,
-                waitForData: _options.WaitForDataBeforeAllocatingBuffer);
+                waitForData: _options.WaitForDataBeforeAllocatingBuffer,
+                finOnError: _options.FinOnError);
 
             connection.Start();
             return connection;
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs
index d0c88c0535f3a1106f962c41f7b93624bfe91fc3..fec0b3bef938afd9efdb16bbe09ec4868319a371 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs
@@ -23,8 +23,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
             MaxWriteBufferSize = transportOptions.MaxWriteBufferSize;
             UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling;
             MemoryPoolFactory = transportOptions.MemoryPoolFactory;
+            FinOnError = transportOptions.FinOnError;
         }
 
+        // Opt-out flag for back compat. Remove in 9.0 (or make public).
+        internal bool FinOnError { get; set; }
+
         /// <summary>
         /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool.
         /// </summary>
diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs
index 9130cb98045407c6f3952e2185b5436dc3a9aae8..4f22c079dc1fe5cfea2577145960bde5ac3913c3 100644
--- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs
+++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs
@@ -13,6 +13,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets
     /// </summary>
     public class SocketTransportOptions
     {
+        private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
+        private static readonly bool _finOnError;
+
+        static SocketTransportOptions()
+        {
+            AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
+        }
+
+        // Opt-out flag for back compat. Remove in 9.0 (or make public).
+        internal bool FinOnError { get; set; } = _finOnError;
+
         /// <summary>
         /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool.
         /// </summary>
diff --git a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs
index 5dd91c4337d9cddb13d344fd4589fdce336fd25d..eb4f310e7a291559b94ab733c20529822832c993 100644
--- a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs
+++ b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs
@@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Testing
         public async Task WaitForConnectionClose()
         {
             var buffer = new byte[128];
-            var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).TimeoutAfter(Timeout);
+        	var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).ContinueWith(t => t.IsFaulted ? 0 : t.Result).TimeoutAfter(Timeout);
 
             if (bytesTransferred > 0)
             {
diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs
index 79fc65849640474021c3cf81095c56be05e84097..0c8109cb99522b601a195ddafb2aee1b5d494f5e 100644
--- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs
+++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs
@@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
         {
         }
 
-        public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions)
+        public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions, Action<IServiceCollection> configureServices = null)
             : this(app, context, options =>
             {
                 var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
@@ -57,7 +57,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 };
                 configureListenOptions(listenOptions);
                 options.CodeBackedListenOptions.Add(listenOptions);
-            }, _ => { })
+            }, s =>
+            {
+                configureServices?.Invoke(s);
+            })
         {
         }
 
diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
index 1a12f4629e444cf49cc96b2cf0605b94afb51173..ff5f06ea21a51fa6d30b4cb65f92163fffb30588 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
@@ -46,6 +46,59 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
             };
         }
 
+        [ConditionalFact]
+        public async Task ConnectionClosedWithoutActiveRequestsOrGoAwayFIN()
+        {
+            var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var readFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var writeFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            TestSink.MessageLogged += context =>
+            {
+                if (context.EventId.Name == "Http2ConnectionClosed")
+                {
+                    connectionClosed.SetResult();
+                }
+                else if (context.EventId.Name == "ConnectionReadFin")
+                {
+                    readFin.SetResult();
+                }
+                else if (context.EventId.Name == "ConnectionWriteFin")
+                {
+                    writeFin.SetResult();
+                }
+            };
+
+            var testContext = new TestServiceContext(LoggerFactory);
+
+            testContext.InitializeHeartbeat();
+
+            await using (var server = new TestServer(context =>
+            {
+                return context.Response.WriteAsync("hello world " + context.Request.Protocol);
+            },
+            testContext,
+            kestrelOptions =>
+            {
+                kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions =>
+                {
+                    listenOptions.Protocols = HttpProtocols.Http2;
+                    listenOptions.UseHttps(_x509Certificate2);
+                });
+            }))
+            {
+                var response = await Client.GetStringAsync($"https://localhost:{server.Port}/");
+                Assert.Equal("hello world HTTP/2", response);
+                Client.Dispose(); // Close the socket, no GoAway is sent.
+
+                await readFin.Task.DefaultTimeout();
+                await writeFin.Task.DefaultTimeout();
+                await connectionClosed.Task.DefaultTimeout();
+
+                await server.StopAsync();
+            }
+        }
+
         [CollectDump]
         [ConditionalFact]
         public async Task GracefulShutdownWaitsForRequestsToFinish()
diff --git a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs
index e10a5dc3ad3d75a2fff50b15e0b17628ffce0bfb..af391389ade33d83d8d6d35d194db87dbae97560 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs
@@ -21,7 +21,13 @@ using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
 using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
+#if LIBUV
+using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv;
+#elif SOCKETS
+using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
+#endif
 using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Moq;
@@ -40,6 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
     public class RequestTests : LoggedTest
     {
         private const int _connectionStartedEventId = 1;
+        private const int _connectionReadFinEventId = 6;
         private const int _connectionResetEventId = 19;
         private static readonly int _semaphoreWaitTimeout = Debugger.IsAttached ? 10000 : 2500;
 
@@ -234,6 +241,59 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             }
         }
 
+        [Fact]
+        public async Task ConnectionClosedPriorToRequestIsLoggedAsDebug()
+        {
+            var connectionStarted = new SemaphoreSlim(0);
+            var connectionReadFin = new SemaphoreSlim(0);
+            var loggedHigherThanDebug = false;
+
+            TestSink.MessageLogged += context =>
+            {
+                if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" &&
+                    context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Connections" &&
+                    context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" &&
+                    context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets")
+                {
+                    return;
+                }
+
+                if (context.EventId.Id == _connectionStartedEventId)
+                {
+                    connectionStarted.Release();
+                }
+                else if (context.EventId.Id == _connectionReadFinEventId)
+                {
+                    connectionReadFin.Release();
+                }
+
+                if (context.LogLevel > LogLevel.Debug)
+                {
+                    loggedHigherThanDebug = true;
+                }
+            };
+
+            await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory)))
+            {
+                using (var connection = server.CreateConnection())
+                {
+                    // Wait until connection is established
+                    Assert.True(await connectionStarted.WaitAsync(TestConstants.DefaultTimeout));
+
+                    connection.ShutdownSend();
+
+                    // If the reset is correctly logged as Debug, the wait below should complete shortly.
+                    // This check MUST come before disposing the server, otherwise there's a race where the RST
+                    // is still in flight when the connection is aborted, leading to the reset never being received
+                    // and therefore not logged.
+                    Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout));
+                    await connection.ReceiveEnd();
+                }
+            }
+
+            Assert.False(loggedHigherThanDebug);
+        }
+
         [Fact]
         public async Task ConnectionResetPriorToRequestIsLoggedAsDebug()
         {
@@ -286,6 +346,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             Assert.False(loggedHigherThanDebug);
         }
 
+        [Fact]
+        public async Task ConnectionClosedBetweenRequestsIsLoggedAsDebug()
+        {
+            var connectionReadFin = new SemaphoreSlim(0);
+            var loggedHigherThanDebug = false;
+
+            TestSink.MessageLogged += context =>
+            {
+                if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" &&
+                    context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" &&
+                    context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets")
+                {
+                    return;
+                }
+
+                if (context.LogLevel > LogLevel.Debug)
+                {
+                    loggedHigherThanDebug = true;
+                }
+
+                if (context.EventId.Id == _connectionReadFinEventId)
+                {
+                    connectionReadFin.Release();
+                }
+            };
+
+            await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory)))
+            {
+                using (var connection = server.CreateConnection())
+                {
+                    await connection.Send(
+                        "GET / HTTP/1.1",
+                        "Host:",
+                        "",
+                        "");
+
+                    // Make sure the response is fully received, so a write failure (e.g. EPIPE) doesn't cause
+                    // a more critical log message.
+                    await connection.Receive(
+                        "HTTP/1.1 200 OK",
+                        "Content-Length: 0",
+                        $"Date: {server.Context.DateHeaderValue}",
+                        "",
+                        "");
+
+                    connection.ShutdownSend();
+
+                    // If the reset is correctly logged as Debug, the wait below should complete shortly.
+                    // This check MUST come before disposing the server, otherwise there's a race where the RST
+                    // is still in flight when the connection is aborted, leading to the reset never being received
+                    // and therefore not logged.
+                    Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout));
+
+                    await connection.ReceiveEnd();
+                }
+            }
+
+            Assert.False(loggedHigherThanDebug);
+        }
+
         [Fact]
         public async Task ConnectionResetBetweenRequestsIsLoggedAsDebug()
         {
@@ -345,10 +465,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             Assert.False(loggedHigherThanDebug);
         }
 
-        [Fact]
-        public async Task ConnectionResetMidRequestIsLoggedAsDebug()
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ConnectionClosedOrResetMidRequestIsLoggedAsDebug(bool close)
         {
             var requestStarted = new SemaphoreSlim(0);
+            var connectionReadFin = new SemaphoreSlim(0);
             var connectionReset = new SemaphoreSlim(0);
             var connectionClosing = new SemaphoreSlim(0);
             var loggedHigherThanDebug = false;
@@ -367,6 +490,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                     loggedHigherThanDebug = true;
                 }
 
+                if (context.EventId.Id == _connectionReadFinEventId)
+                {
+                    connectionReadFin.Release();
+                }
+
                 if (context.EventId.Id == _connectionResetEventId)
                 {
                     connectionReset.Release();
@@ -387,15 +515,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                     // Wait until connection is established
                     Assert.True(await requestStarted.WaitAsync(TestConstants.DefaultTimeout), "request should have started");
 
-                    connection.Reset();
-                }
+                    if (close)
+                    {
+                        connection.ShutdownSend();
+                        Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout), "Connection close event should have been logged");
+                    }
+                    else
+                    {
+                        connection.Reset();
 
-                // If the reset is correctly logged as Debug, the wait below should complete shortly.
-                // This check MUST come before disposing the server, otherwise there's a race where the RST
-                // is still in flight when the connection is aborted, leading to the reset never being received
-                // and therefore not logged.
-                Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged");
-                connectionClosing.Release();
+                        // If the reset is correctly logged as Debug, the wait below should complete shortly.
+                        // This check MUST come before disposing the server, otherwise there's a race where the RST
+                        // is still in flight when the connection is aborted, leading to the reset never being received
+                        // and therefore not logged.
+                        Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged");
+                    }
+                    connectionClosing.Release();
+                }
             }
 
             Assert.False(loggedHigherThanDebug, "Logged event should not have been higher than debug.");
@@ -494,18 +630,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             }
         }
 
-        [Fact]
-        public async Task AbortingTheConnectionSendsFIN()
+        [Theory]
+#if LIBUV
+        [InlineData(true, 1)]
+        [InlineData(false, 1)]
+        [InlineData(true, 2)]
+        [InlineData(false, 2)]
+        public async Task AbortingTheConnection(bool fin, int threadCount)
+#else
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task AbortingTheConnection(bool fin)
+#endif
         {
+            var connectionAborted = new SemaphoreSlim(0);
+
             var builder = TransportSelector.GetHostBuilder()
+#if LIBUV
+                .ConfigureServices(services =>
+                {
+#pragma warning disable CS0618 // Type or member is obsolete
+                    services.Configure<LibuvTransportOptions>(options =>
+                    {
+                        options.ThreadCount = threadCount;
+                    });
+#pragma warning restore CS0618 // Type or member is obsolete
+                })
+#endif
                 .ConfigureWebHost(webHostBuilder =>
                 {
                     webHostBuilder
+                        .ConfigureServices(s => SetFinOnError(s, fin))
                         .UseKestrel()
                         .UseUrls("http://127.0.0.1:0")
                         .Configure(app => app.Run(context =>
                         {
                             context.Abort();
+                            connectionAborted.Release();
                             return Task.CompletedTask;
                         }));
                 })
@@ -519,8 +680,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 {
                     socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort()));
                     socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"));
-                    int result = socket.Receive(new byte[32]);
-                    Assert.Equal(0, result);
+
+                    Assert.True(await connectionAborted.WaitAsync(_semaphoreWaitTimeout));
+
+                    if (fin)
+                    {
+                        int result = socket.Receive(new byte[32]);
+                        Assert.Equal(0, result);
+                    }
+                    else
+                    {
+                        Assert.Throws<SocketException>(() => socket.Receive(new byte[32]));
+                    }
                 }
 
                 await host.StopAsync();
@@ -731,16 +902,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
         }
 
         [Theory]
-        [MemberData(nameof(ConnectionMiddlewareDataName))]
-        public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName)
+        [InlineData("Loopback", true)]
+        [InlineData("PassThrough", true)]
+        [InlineData("Loopback", false)]
+        [InlineData("PassThrough", false)]
+        public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName, bool fin)
         {
             const int connectionPausedEventId = 4;
             const int connectionFinSentEventId = 7;
+            const int connectionRstSentEventId = 8;
             const int maxRequestBufferSize = 4096;
 
             var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
-            var serverClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var serverFinConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var serverRstConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
             TestSink.MessageLogged += context =>
@@ -757,7 +933,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 }
                 else if (context.EventId == connectionFinSentEventId)
                 {
-                    serverClosedConnection.SetResult();
+                    serverFinConnection.SetResult();
+                }
+                else if (context.EventId == connectionRstSentEventId)
+                {
+                    serverRstConnection.SetResult();
                 }
             };
 
@@ -766,6 +946,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             {
                 ServerOptions =
                 {
+                    FinOnError = fin,
                     Limits =
                     {
                         MaxRequestBufferSize = maxRequestBufferSize,
@@ -783,10 +964,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
 
                 context.Abort();
 
-                await serverClosedConnection.Task;
+                if (fin)
+                {
+                    await serverFinConnection.Task.DefaultTimeout();
+                }
+                else
+                {
+                    await serverRstConnection.Task.DefaultTimeout();
+                }
 
                 appFuncCompleted.SetResult();
-            }, testContext, ConnectionMiddlewareData[listenOptionsName]()))
+            }, testContext, listen =>
+            {
+                if (listenOptionsName == "PassThrough")
+                {
+                    listen.UsePassThrough();
+                }
+            },
+            services => SetFinOnError(services, fin)))
             {
                 using (var connection = server.CreateConnection())
                 {
@@ -956,21 +1151,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
         private static async Task AssertStreamContains(Stream stream, string expectedSubstring)
         {
             var expectedBytes = Encoding.ASCII.GetBytes(expectedSubstring);
-            var exptectedLength = expectedBytes.Length;
-            var responseBuffer = new byte[exptectedLength];
+            var expectedLength = expectedBytes.Length;
+            var responseBuffer = new byte[expectedLength];
 
             var matchedChars = 0;
 
-            while (matchedChars < exptectedLength)
+            while (matchedChars < expectedLength)
             {
-                var count = await stream.ReadAsync(responseBuffer, 0, exptectedLength - matchedChars).DefaultTimeout();
+                var count = await stream.ReadAsync(responseBuffer, 0, expectedLength - matchedChars).DefaultTimeout();
 
                 if (count == 0)
                 {
                     Assert.True(false, "Stream completed without expected substring.");
                 }
 
-                for (var i = 0; i < count && matchedChars < exptectedLength; i++)
+                for (var i = 0; i < count && matchedChars < expectedLength; i++)
                 {
                     if (responseBuffer[i] == expectedBytes[matchedChars])
                     {
@@ -983,5 +1178,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 }
             }
         }
+
+        private static void SetFinOnError(IServiceCollection services, bool finOnError)
+        {
+#if LIBUV
+#pragma warning disable CS0618 // Type or member is obsolete
+            services.Configure<LibuvTransportOptions>(options =>
+            {
+                options.FinOnError = finOnError;
+            });
+#pragma warning restore CS0618 // Type or member is obsolete
+#elif SOCKETS
+            services.Configure<SocketTransportOptions>(o =>
+            {
+                o.FinOnError = finOnError;
+            });
+#endif
+            services.Configure<KestrelServerOptions>(o =>
+            {
+                o.FinOnError = finOnError;
+            });
+        }
     }
 }
diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs
index 991044dbf8f05b7154b459f04f3349710357809b..cbda94bd05a61aa555a28494a4f518ff457b9178 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs
@@ -23,7 +23,13 @@ using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
 using Microsoft.AspNetCore.Server.Kestrel.Https;
 using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
 using Microsoft.AspNetCore.Server.Kestrel.Tests;
+#if LIBUV
+using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv;
+#elif SOCKETS
+using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
+#endif
 using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Testing;
@@ -462,8 +468,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             Assert.Empty(coreLogs.Where(w => w.LogLevel > LogLevel.Information));
         }
 
-        [Fact]
-        public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate()
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin)
         {
             var logger = LoggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}");
             const int chunkSize = 1024;
@@ -473,21 +481,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
 
             var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
-            var mockKestrelTrace = new Mock<IKestrelTrace>();
-            mockKestrelTrace
-                .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>()))
-                .Callback(() => responseRateTimeoutMessageLogged.SetResult());
-            mockKestrelTrace
-                .Setup(trace => trace.ConnectionStop(It.IsAny<string>()))
-                .Callback(() => connectionStopMessageLogged.SetResult());
+            TestSink.MessageLogged += context =>
+            {
+                switch (context.EventId.Name)
+                {
+                    case "ResponseMinimumDataRateNotSatisfied":
+                        responseRateTimeoutMessageLogged.SetResult();
+                        break;
+                    case "ConnectionStop":
+                        connectionStopMessageLogged.SetResult();
+                        break;
+                    case "ConnectionWriteFin":
+                        connectionWriteFinMessageLogged.SetResult();
+                        break;
+                    case "ConnectionWriteRst":
+                        connectionWriteRstMessageLogged.SetResult();
+                        break;
+                }
+            };
 
-            var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)
+            var testContext = new TestServiceContext(LoggerFactory)
             {
                 ServerOptions =
                 {
+                    FinOnError = fin,
                     Limits =
                     {
                         MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2))
@@ -533,7 +555,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 }
             }
 
-            await using (var server = new TestServer(App, testContext))
+            await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, services => SetFinOnError(services, fin)))
             {
                 using (var connection = server.CreateConnection())
                 {
@@ -553,8 +575,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                     await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30));
                     await responseRateTimeoutMessageLogged.Task.DefaultTimeout();
                     await connectionStopMessageLogged.Task.DefaultTimeout();
+                    if (fin)
+                    {
+                        await connectionWriteFinMessageLogged.Task.DefaultTimeout();
+                    }
+                    else
+                    {
+                        await connectionWriteRstMessageLogged.Task.DefaultTimeout();
+                    }
                     await appFuncCompleted.Task.DefaultTimeout();
-                    await AssertStreamAborted(connection.Stream, chunkSize * chunks);
+                    await AssertStreamAborted(connection.Stream, chunkSize * chunks, fin);
 
                     sw.Stop();
                     logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds);
@@ -562,8 +592,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             }
         }
 
-        [Fact]
-        public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate()
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin)
         {
             const int chunkSize = 1024;
             const int chunks = 256 * 1024;
@@ -573,21 +605,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
 
             var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
-            var mockKestrelTrace = new Mock<IKestrelTrace>();
-            mockKestrelTrace
-                .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>()))
-                .Callback(() => responseRateTimeoutMessageLogged.SetResult());
-            mockKestrelTrace
-                .Setup(trace => trace.ConnectionStop(It.IsAny<string>()))
-                .Callback(() => connectionStopMessageLogged.SetResult());
+            TestSink.MessageLogged += context =>
+            {
+                switch (context.EventId.Name)
+                {
+                    case "ResponseMinimumDataRateNotSatisfied":
+                        responseRateTimeoutMessageLogged.SetResult();
+                        break;
+                    case "ConnectionStop":
+                        connectionStopMessageLogged.SetResult();
+                        break;
+                    case "ConnectionWriteFin":
+                        connectionWriteFinMessageLogged.SetResult();
+                        break;
+                    case "ConnectionWriteRst":
+                        connectionWriteRstMessageLogged.SetResult();
+                        break;
+                }
+            };
 
-            var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)
+            var testContext = new TestServiceContext(LoggerFactory)
             {
                 ServerOptions =
                 {
+                    FinOnError = fin,
                     Limits =
                     {
                         MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2))
@@ -627,7 +673,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 {
                     await aborted.Task.DefaultTimeout();
                 }
-            }, testContext, ConfigureListenOptions))
+            }, testContext, ConfigureListenOptions,
+            services => SetFinOnError(services, fin)))
             {
                 using (var connection = server.CreateConnection())
                 {
@@ -642,16 +689,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                         await aborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30));
                         await responseRateTimeoutMessageLogged.Task.DefaultTimeout();
                         await connectionStopMessageLogged.Task.DefaultTimeout();
+                        if (fin)
+                        {
+                            await connectionWriteFinMessageLogged.Task.DefaultTimeout();
+                        }
+                        else
+                        {
+                            await connectionWriteRstMessageLogged.Task.DefaultTimeout();
+                        }
                         await appFuncCompleted.Task.DefaultTimeout();
 
-                        await AssertStreamAborted(connection.Stream, chunkSize * chunks);
+                        await AssertStreamAborted(connection.Stream, chunkSize * chunks, fin);
                     }
                 }
             }
         }
 
-        [Fact]
-        public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure()
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure(bool fin)
         {
             const int bufferSize = 65536;
             const int bufferCount = 100;
@@ -660,21 +717,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
 
             var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+            var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             var copyToAsyncCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
-            var mockKestrelTrace = new Mock<IKestrelTrace>();
-            mockKestrelTrace
-                .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>()))
-                .Callback(() => responseRateTimeoutMessageLogged.SetResult());
-            mockKestrelTrace
-                .Setup(trace => trace.ConnectionStop(It.IsAny<string>()))
-                .Callback(() => connectionStopMessageLogged.SetResult());
+            TestSink.MessageLogged += context =>
+            {
+                switch (context.EventId.Name)
+                {
+                    case "ResponseMinimumDataRateNotSatisfied":
+                        responseRateTimeoutMessageLogged.SetResult();
+                        break;
+                    case "ConnectionStop":
+                        connectionStopMessageLogged.SetResult();
+                        break;
+                    case "ConnectionWriteFin":
+                        connectionWriteFinMessageLogged.SetResult();
+                        break;
+                    case "ConnectionWriteRst":
+                        connectionWriteRstMessageLogged.SetResult();
+                        break;
+                }
+            };
 
-            var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object)
+            var testContext = new TestServiceContext(LoggerFactory)
             {
                 ServerOptions =
                 {
+                    FinOnError = fin,
                     Limits =
                     {
                         MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)),
@@ -685,8 +756,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
 
             testContext.InitializeHeartbeat();
 
-            var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0));
-
             async Task App(HttpContext context)
             {
                 context.RequestAborted.Register(() =>
@@ -711,7 +780,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 copyToAsyncCts.SetException(new Exception("This shouldn't be reached."));
             }
 
-            await using (var server = new TestServer(App, testContext, listenOptions))
+            await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, services => SetFinOnError(services, fin)))
             {
                 using (var connection = server.CreateConnection())
                 {
@@ -736,10 +805,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                     await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30));
                     await responseRateTimeoutMessageLogged.Task.DefaultTimeout();
                     await connectionStopMessageLogged.Task.DefaultTimeout();
+                    if (fin)
+                    {
+                        await connectionWriteFinMessageLogged.Task.DefaultTimeout();
+                    }
+                    else
+                    {
+                        await connectionWriteRstMessageLogged.Task.DefaultTimeout();
+                    }
 
                     // Expect OperationCanceledException instead of IOException because the server initiated the abort due to a response rate timeout.
                     await Assert.ThrowsAnyAsync<OperationCanceledException>(() => copyToAsyncCts.Task).DefaultTimeout();
-                    await AssertStreamAborted(connection.Stream, responseSize);
+                    await AssertStreamAborted(connection.Stream, responseSize, graceful: false);
                 }
             }
         }
@@ -985,7 +1062,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             Assert.False(requestAborted);
         }
 
-        private async Task AssertStreamAborted(Stream stream, int totalBytes)
+        private async Task AssertStreamAborted(Stream stream, int totalBytes, bool graceful)
         {
             var receiveBuffer = new byte[64 * 1024];
             var totalReceived = 0;
@@ -998,6 +1075,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
 
                     if (bytes == 0)
                     {
+                        Assert.True(graceful, "Stream completed gracefully.");
+
                         break;
                     }
 
@@ -1006,7 +1085,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
             }
             catch (IOException)
             {
-                // This is expected given an abort.
+                Assert.False(graceful, "Stream completed abortively.");
             }
 
             Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully.");
@@ -1080,5 +1159,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
                 return dataset;
             }
         }
+
+        private static void SetFinOnError(IServiceCollection services, bool finOnError)
+        {
+#if LIBUV
+#pragma warning disable CS0618 // Type or member is obsolete
+            services.Configure<LibuvTransportOptions>(options =>
+            {
+                options.FinOnError = finOnError;
+            });
+#pragma warning restore CS0618 // Type or member is obsolete
+#elif SOCKETS
+            services.Configure<SocketTransportOptions>(o =>
+            {
+                o.FinOnError = finOnError;
+            });
+#endif
+            services.Configure<KestrelServerOptions>(o =>
+            {
+                o.FinOnError = finOnError;
+            });
+        }
     }
 }