From 0a0e5e0ad90bf3a6da5467341974b012d2ceb613 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 8 May 2026 13:38:10 +0000
Subject: [PATCH 1/3] Initial plan
From 56f9e2a744174fd6291b3d7e0256955a69699edc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 8 May 2026 13:44:31 +0000
Subject: [PATCH 2/3] Make MaxPendingReads configurable on SftpClient to
prevent connection freezes on resource-constrained platforms
Add MaxPendingReads property to ISftpClient and SftpClient (default 100 for backward compatibility).
Pass the value through SftpFileStream.Open/OpenAsync instead of using a hardcoded constant.
Users on Android or other constrained platforms can now reduce this value (e.g., to 10) to prevent freezes when downloading larger files.
Agent-Logs-Url: https://github.com/sshnet/SSH.NET/sessions/b4b9b5e6-e214-41a2-9efd-0c2a63f65426
Co-authored-by: WojciechNagorski <17333903+WojciechNagorski@users.noreply.github.com>
---
src/Renci.SshNet/ISftpClient.cs | 21 +++++++++
src/Renci.SshNet/Sftp/SftpFileStream.cs | 27 +++++++-----
src/Renci.SshNet/SftpClient.cs | 57 ++++++++++++++++++++++---
3 files changed, 89 insertions(+), 16 deletions(-)
diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs
index 7b1237bc2..664986cbb 100644
--- a/src/Renci.SshNet/ISftpClient.cs
+++ b/src/Renci.SshNet/ISftpClient.cs
@@ -49,6 +49,27 @@ public interface ISftpClient : IBaseClient
/// The method was called after the client was disposed.
uint BufferSize { get; set; }
+ ///
+ /// Gets or sets the maximum number of pending read requests allowed in read-ahead mode.
+ ///
+ ///
+ /// The maximum number of pending read requests. The default value is 100.
+ ///
+ ///
+ ///
+ /// This controls how many SSH_FXP_READ requests can be in-flight simultaneously
+ /// when sequentially reading a file. Higher values allow the library to pipeline
+ /// more requests, improving throughput on high-latency connections.
+ ///
+ ///
+ /// On resource-constrained platforms (e.g., mobile devices), reducing this value
+ /// can prevent connection stalls when downloading larger files.
+ ///
+ ///
+ /// is less than 1.
+ /// The method was called after the client was disposed.
+ int MaxPendingReads { get; set; }
+
///
/// Gets or sets the operation timeout.
///
diff --git a/src/Renci.SshNet/Sftp/SftpFileStream.cs b/src/Renci.SshNet/Sftp/SftpFileStream.cs
index d8c0d126f..ce26a0ca3 100644
--- a/src/Renci.SshNet/Sftp/SftpFileStream.cs
+++ b/src/Renci.SshNet/Sftp/SftpFileStream.cs
@@ -18,7 +18,7 @@ namespace Renci.SshNet.Sftp
///
public sealed partial class SftpFileStream : Stream
{
- private const int MaxPendingReads = 100;
+ private readonly int _maxPendingReads;
private readonly ISftpSession _session;
private readonly FileAccess _access;
@@ -140,6 +140,7 @@ private SftpFileStream(
int writeBufferSize,
byte[] handle,
long position,
+ int maxPendingReads,
SftpFileReader? initialReader)
{
Timeout = TimeSpan.FromSeconds(30);
@@ -148,6 +149,7 @@ private SftpFileStream(
_session = session;
_access = access;
_canSeek = canSeek;
+ _maxPendingReads = maxPendingReads;
Handle = handle;
_readBufferSize = readBufferSize;
@@ -163,9 +165,10 @@ internal static SftpFileStream Open(
FileMode mode,
FileAccess access,
int bufferSize,
- bool isDownloadFile = false)
+ bool isDownloadFile = false,
+ int maxPendingReads = 100)
{
- return Open(session, path, mode, access, bufferSize, isDownloadFile, isAsync: false, CancellationToken.None).GetAwaiter().GetResult();
+ return Open(session, path, mode, access, bufferSize, maxPendingReads, isDownloadFile, isAsync: false, CancellationToken.None).GetAwaiter().GetResult();
}
internal static Task OpenAsync(
@@ -175,9 +178,10 @@ internal static Task OpenAsync(
FileAccess access,
int bufferSize,
CancellationToken cancellationToken,
- bool isDownloadFile = false)
+ bool isDownloadFile = false,
+ int maxPendingReads = 100)
{
- return Open(session, path, mode, access, bufferSize, isDownloadFile, isAsync: true, cancellationToken);
+ return Open(session, path, mode, access, bufferSize, maxPendingReads, isDownloadFile, isAsync: true, cancellationToken);
}
private static async Task Open(
@@ -186,6 +190,7 @@ private static async Task Open(
FileMode mode,
FileAccess access,
int bufferSize,
+ int maxPendingReads,
bool isDownloadFile,
bool isAsync,
CancellationToken cancellationToken)
@@ -309,15 +314,15 @@ private static async Task Open(
// so we can let there be several in-flight requests from the get go.
// This optimisation is mostly only beneficial to smaller files on higher latency connections.
// The +2 is +1 for rounding up to cover the whole file, and +1 for the final request to receive EOF.
- var initialPendingReads = (int)Math.Max(1, Math.Min(MaxPendingReads, 2 + (attributes.Size / readBufferSize)));
+ var initialPendingReads = (int)Math.Max(1, Math.Min(maxPendingReads, 2 + (attributes.Size / readBufferSize)));
- initialReader = new(handle, session, readBufferSize, position, MaxPendingReads, (ulong)attributes.Size, initialPendingReads);
+ initialReader = new(handle, session, readBufferSize, position, maxPendingReads, (ulong)attributes.Size, initialPendingReads);
}
else if ((access & FileAccess.Read) == FileAccess.Read)
{
// The reader can use the size information to reduce in-flight requests near the expected EOF,
// so pass it in here.
- initialReader = new(handle, session, readBufferSize, position, MaxPendingReads, (ulong)attributes.Size);
+ initialReader = new(handle, session, readBufferSize, position, maxPendingReads, (ulong)attributes.Size);
}
}
else
@@ -327,7 +332,7 @@ private static async Task Open(
canSeek = false;
}
- return new SftpFileStream(session, path, access, canSeek, readBufferSize, writeBufferSize, handle, position, initialReader);
+ return new SftpFileStream(session, path, access, canSeek, readBufferSize, writeBufferSize, handle, position, maxPendingReads, initialReader);
}
///
@@ -421,7 +426,7 @@ private int Read(Span buffer)
if (_sftpFileReader is null)
{
Flush();
- _sftpFileReader = new(Handle, _session, _readBufferSize, _position, MaxPendingReads);
+ _sftpFileReader = new(Handle, _session, _readBufferSize, _position, _maxPendingReads);
}
_readBuffer = _sftpFileReader.ReadAsync(CancellationToken.None).GetAwaiter().GetResult();
@@ -475,7 +480,7 @@ private async ValueTask ReadAsync(Memory buffer, CancellationToken ca
{
await FlushAsync(cancellationToken).ConfigureAwait(false);
- _sftpFileReader = new(Handle, _session, _readBufferSize, _position, MaxPendingReads);
+ _sftpFileReader = new(Handle, _session, _readBufferSize, _position, _maxPendingReads);
}
_readBuffer = await _sftpFileReader.ReadAsync(cancellationToken).ConfigureAwait(false);
diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs
index 960c6261f..ca7362575 100644
--- a/src/Renci.SshNet/SftpClient.cs
+++ b/src/Renci.SshNet/SftpClient.cs
@@ -42,6 +42,11 @@ public class SftpClient : BaseClient, ISftpClient
///
private uint _bufferSize;
+ ///
+ /// Holds the maximum number of pending reads.
+ ///
+ private int _maxPendingReads;
+
///
/// Gets or sets the operation timeout.
///
@@ -112,6 +117,45 @@ public uint BufferSize
}
}
+ ///
+ /// Gets or sets the maximum number of pending read requests allowed in read-ahead mode.
+ ///
+ ///
+ /// The maximum number of pending read requests. The default value is 100.
+ ///
+ ///
+ ///
+ /// This controls how many SSH_FXP_READ requests can be in-flight simultaneously
+ /// when sequentially reading a file. Higher values allow the library to pipeline
+ /// more requests, improving throughput on high-latency connections.
+ ///
+ ///
+ /// On resource-constrained platforms (e.g., mobile devices), reducing this value
+ /// can prevent connection stalls when downloading larger files.
+ ///
+ ///
+ /// is less than 1.
+ /// The method was called after the client was disposed.
+ public int MaxPendingReads
+ {
+ get
+ {
+ CheckDisposed();
+ return _maxPendingReads;
+ }
+ set
+ {
+ CheckDisposed();
+
+ if (value < 1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "Cannot be less than one.");
+ }
+
+ _maxPendingReads = value;
+ }
+ }
+
///
/// Gets a value indicating whether this client is connected to the server and
/// the SFTP session is open.
@@ -279,6 +323,7 @@ internal SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, ISer
{
_operationTimeout = Timeout.Infinite;
_bufferSize = 1024 * 32;
+ _maxPendingReads = 100;
}
#endregion Constructors
@@ -1544,7 +1589,7 @@ public SftpFileStream Create(string path, int bufferSize)
{
CheckDisposed();
- return SftpFileStream.Open(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize);
+ return SftpFileStream.Open(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize, maxPendingReads: _maxPendingReads);
}
///
@@ -1682,7 +1727,7 @@ public SftpFileStream Open(string path, FileMode mode, FileAccess access)
{
CheckDisposed();
- return SftpFileStream.Open(_sftpSession, path, mode, access, (int)_bufferSize);
+ return SftpFileStream.Open(_sftpSession, path, mode, access, (int)_bufferSize, maxPendingReads: _maxPendingReads);
}
///
@@ -1703,7 +1748,7 @@ public Task OpenAsync(string path, FileMode mode, FileAccess acc
{
CheckDisposed();
- return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken);
+ return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken, maxPendingReads: _maxPendingReads);
}
///
@@ -2357,7 +2402,8 @@ private async Task InternalDownloadFile(
FileAccess.Read,
(int)_bufferSize,
cancellationToken,
- isDownloadFile: true).ConfigureAwait(false);
+ isDownloadFile: true,
+ maxPendingReads: _maxPendingReads).ConfigureAwait(false);
}
else
{
@@ -2369,7 +2415,8 @@ private async Task InternalDownloadFile(
FileMode.Open,
FileAccess.Read,
(int)_bufferSize,
- isDownloadFile: true);
+ isDownloadFile: true,
+ maxPendingReads: _maxPendingReads);
}
// The below is effectively sftpStream.CopyTo{Async}(output) with consideration
From 8faef0735603b9717b2fff38bd997eee6acd340b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 8 May 2026 13:47:30 +0000
Subject: [PATCH 3/3] Fix XML doc comments for MaxPendingReads property
exceptions
Agent-Logs-Url: https://github.com/sshnet/SSH.NET/sessions/b4b9b5e6-e214-41a2-9efd-0c2a63f65426
Co-authored-by: WojciechNagorski <17333903+WojciechNagorski@users.noreply.github.com>
---
src/Renci.SshNet/ISftpClient.cs | 2 +-
src/Renci.SshNet/SftpClient.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Renci.SshNet/ISftpClient.cs b/src/Renci.SshNet/ISftpClient.cs
index 664986cbb..f79ae9c4c 100644
--- a/src/Renci.SshNet/ISftpClient.cs
+++ b/src/Renci.SshNet/ISftpClient.cs
@@ -66,7 +66,7 @@ public interface ISftpClient : IBaseClient
/// can prevent connection stalls when downloading larger files.
///
///
- /// is less than 1.
+ /// The value is less than 1.
/// The method was called after the client was disposed.
int MaxPendingReads { get; set; }
diff --git a/src/Renci.SshNet/SftpClient.cs b/src/Renci.SshNet/SftpClient.cs
index ca7362575..194d79f80 100644
--- a/src/Renci.SshNet/SftpClient.cs
+++ b/src/Renci.SshNet/SftpClient.cs
@@ -134,7 +134,7 @@ public uint BufferSize
/// can prevent connection stalls when downloading larger files.
///
///
- /// is less than 1.
+ /// The value is less than 1.
/// The method was called after the client was disposed.
public int MaxPendingReads
{