Is it possible to track the progress of a download using HttpClient (C#)?

Yes, you can track the progress of downloads using HttpClient in C#. This requires manually handling the response stream and reading data in chunks to monitor bytes transferred over time.

Key Implementation Steps

  1. Use HttpCompletionOption.ResponseHeadersRead to start processing immediately
  2. Check Content-Length header for total download size (when available)
  3. Read the response stream in chunks to track progress
  4. Implement progress reporting through callbacks or events

Basic Progress Tracking Example

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

public class DownloadProgressTracker
{
    private readonly HttpClient _httpClient;

    public DownloadProgressTracker()
    {
        _httpClient = new HttpClient();
    }

    public async Task DownloadFileAsync(string url, string filePath, 
        IProgress<DownloadProgress> progress = null)
    {
        using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);

        if (!response.IsSuccessStatusCode)
            throw new HttpRequestException($"Download failed: {response.StatusCode}");

        var totalBytes = response.Content.Headers.ContentLength ?? -1L;
        var totalBytesRead = 0L;
        var buffer = new byte[8192];

        using var contentStream = await response.Content.ReadAsStreamAsync();
        using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, 
            FileShare.None, bufferSize: 8192, useAsync: true);

        int bytesRead;
        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await fileStream.WriteAsync(buffer, 0, bytesRead);
            totalBytesRead += bytesRead;

            // Report progress
            progress?.Report(new DownloadProgress(totalBytesRead, totalBytes));
        }
    }
}

public class DownloadProgress
{
    public long BytesDownloaded { get; }
    public long TotalBytes { get; }
    public double? ProgressPercentage => TotalBytes > 0 ? 
        (double)BytesDownloaded / TotalBytes * 100 : null;

    public DownloadProgress(long bytesDownloaded, long totalBytes)
    {
        BytesDownloaded = bytesDownloaded;
        TotalBytes = totalBytes;
    }
}

Usage Example

class Program
{
    static async Task Main(string[] args)
    {
        var downloader = new DownloadProgressTracker();
        var progress = new Progress<DownloadProgress>(p =>
        {
            if (p.ProgressPercentage.HasValue)
            {
                Console.WriteLine($"Downloaded: {p.BytesDownloaded:N0} / {p.TotalBytes:N0} bytes " +
                                $"({p.ProgressPercentage:F1}%)");
            }
            else
            {
                Console.WriteLine($"Downloaded: {p.BytesDownloaded:N0} bytes (size unknown)");
            }
        });

        try
        {
            await downloader.DownloadFileAsync(
                "https://example.com/largefile.zip", 
                "download.zip", 
                progress);

            Console.WriteLine("Download completed!");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Download failed: {ex.Message}");
        }
    }
}

Advanced Example with Cancellation and Speed Calculation

using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class AdvancedDownloader
{
    private readonly HttpClient _httpClient;

    public AdvancedDownloader()
    {
        _httpClient = new HttpClient();
    }

    public async Task DownloadWithProgressAsync(string url, string filePath,
        IProgress<DetailedProgress> progress = null, 
        CancellationToken cancellationToken = default)
    {
        using var response = await _httpClient.GetAsync(url, 
            HttpCompletionOption.ResponseHeadersRead, cancellationToken);

        response.EnsureSuccessStatusCode();

        var totalBytes = response.Content.Headers.ContentLength ?? -1L;
        var totalBytesRead = 0L;
        var buffer = new byte[16384]; // 16KB buffer
        var stopwatch = Stopwatch.StartNew();

        using var contentStream = await response.Content.ReadAsStreamAsync();
        using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, 
            FileShare.None, bufferSize: 16384, useAsync: true);

        int bytesRead;
        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length, 
            cancellationToken)) > 0)
        {
            await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
            totalBytesRead += bytesRead;

            // Calculate speed
            var elapsedTime = stopwatch.Elapsed;
            var speedBytesPerSecond = elapsedTime.TotalSeconds > 0 ? 
                totalBytesRead / elapsedTime.TotalSeconds : 0;

            progress?.Report(new DetailedProgress(
                totalBytesRead, 
                totalBytes, 
                speedBytesPerSecond, 
                elapsedTime));
        }
    }
}

public class DetailedProgress
{
    public long BytesDownloaded { get; }
    public long TotalBytes { get; }
    public double SpeedBytesPerSecond { get; }
    public TimeSpan ElapsedTime { get; }

    public double? ProgressPercentage => TotalBytes > 0 ? 
        (double)BytesDownloaded / TotalBytes * 100 : null;

    public string SpeedFormatted => SpeedBytesPerSecond switch
    {
        >= 1_073_741_824 => $"{SpeedBytesPerSecond / 1_073_741_824:F1} GB/s",
        >= 1_048_576 => $"{SpeedBytesPerSecond / 1_048_576:F1} MB/s",
        >= 1024 => $"{SpeedBytesPerSecond / 1024:F1} KB/s",
        _ => $"{SpeedBytesPerSecond:F0} B/s"
    };

    public DetailedProgress(long bytesDownloaded, long totalBytes, 
        double speedBytesPerSecond, TimeSpan elapsedTime)
    {
        BytesDownloaded = bytesDownloaded;
        TotalBytes = totalBytes;
        SpeedBytesPerSecond = speedBytesPerSecond;
        ElapsedTime = elapsedTime;
    }
}

Important Considerations

Content-Length Header

Not all servers provide the Content-Length header. When unavailable: - You can still track bytes downloaded - Progress percentage cannot be calculated - Consider implementing time-based progress indicators

Buffer Size Optimization

  • Small buffers (1-4KB): More frequent progress updates, higher CPU overhead
  • Large buffers (16-64KB): Better performance, less frequent updates
  • Recommended: 8-16KB for balanced performance and responsiveness

Error Handling

Always implement proper error handling for: - Network timeouts - Disk space issues - HTTP error responses - Cancellation requests

Memory Management

Use using statements or proper disposal to avoid memory leaks with streams and HttpClient responses.

This approach provides flexible, efficient download progress tracking that works well for both small and large file downloads while maintaining responsive user interfaces.

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon