# Track transferred bytes/progress of longer PUT request with std.http

**URL:** <https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445>\
**Category:** Help\
**Tags:** standard-library, http\
**Created:** [July 1, 2026, 10:58am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445 "2026-07-01T10:58:27Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 1, 2026, 10:58am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/1 "2026-07-01T10:58:27Z")

</div>

Hey,

I want to send bigger chunks of data from a local machine to a storage system using `std.http` methods. Because the data can be up to some tens or even hundred GiB, it would be nice to track the progress so the user can receive some feedback how long it might take.

But since I’m not very experienced with low-level network stuff, I’m hoping for some tipps.

Whats a good way to accomplish that with std-lib methods? I’ve read through some implementations like `std.http.BodyWriter`, `std.http.Client.Request/Response` etc. But since I’m just not very familiar with the underlying concepts, I’m a bit lost. Furthermore, I’m locked in to `std.http` for now, so custom http libs implemented by other users are no option at the moment.

I’m thankful for any hint; and be it only where to look further. 🙂

---

<div class="post-metadata">

**Author:** ![Bobvan](https://ziggit.dev/user_avatar/ziggit.dev/bobvan/32/5105_2.png) [@Bobvan](https://ziggit.dev/u/Bobvan)\
**Post date:** [July 1, 2026, 11:23am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/2 "2026-07-01T11:23:56Z")

</div>

Hello, i might be out of touch, but std had HTTP/1 implementation.  
If you are actually transfering in range of 100 GiB, you might want to consider HTTP/2.  
At that point, some dependency will be required.

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 1, 2026, 11:52am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/3 "2026-07-01T11:52:30Z")

</div>

Thanks for the hint. I don’t know the real size ranges now. But they definitely _can_ be in the multi-GiB range. However, the storage system uses HTTP/1.1, which should be fine with `std.http` if I’m not wrong.

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [July 1, 2026, 5:28pm UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/4 "2026-07-01T17:28:08Z")

</div>

If you are sending data using the HTTP client, you need to give it a reader for the body, no? So you implement a custom reader that forwards all vtable functions to the original one, but tracks progress.

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 1, 2026, 6:26pm UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/5 "2026-07-01T18:26:10Z")

</div>

Ok, that sounds like a good solution, thank you. Does anyone knows of an example/short tutorial how to implement such a custom reader which forwards the vtable functions. While this might be easy/trivial for many programmers, thats what I meant with “concepts” I’m unfamiliar with (while I can use Zig for my own stuff even at work, Im still just a hobby programmer).

---

<div class="post-metadata">

**Author:** ![psznm](https://ziggit.dev/user_avatar/ziggit.dev/psznm/32/8377_2.png) [@psznm](https://ziggit.dev/u/psznm)\
**Post date:** [July 1, 2026, 6:34pm UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/6 "2026-07-01T18:34:54Z")

</div>

Check the implementation of std.Io.net.Stream.Reader (and Writer) It is pretty easy to understand how to implement your own reader or writer from that. You need to implement only one method for each - the one which have no default implementation in the vtable, but since you will probably just be wrapping the Stream variants, you might want to wrap both of the methods Stream implements.

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 2, 2026, 5:47am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/7 "2026-07-02T05:47:43Z")

</div>

> [@psznm](#):
>
> Check the implementation of std.Io.net.Stream.Reader (and Writer) It is pretty easy to understand how to implement your own reader or writer from that.

Hi, thanks you for the hint. I’v read through the [source code](https://ziglang.org/documentation/0.16.0/std/#std.Io.net.Stream.Reader) and it seems be doable. However, I’m not really sure how/where to track the data progress and, first of all, how to make the `Connection` of `std.http.Client` use my custom implementation. The field [`Connection.stream_reader`](https://ziglang.org/documentation/0.16.0/std/#std.http.Client.Connection) expects type `std.Io.net.Stream.Reader`, how can I put a custom implementation here?

Its absolutely possible that I’m missing something very obvious, because, as stated, I’m not very familiar with this basic concepts of programming languages (vtable’s etc). Unfortunately, therefore its not easy to understand for me 🙃 (for now, I hope). Thus, I’m happy for any additional info how to accomplish that. But of course, no one should feel urged to do so. Its not the end of the world if the progress won’t get tracked 😁

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [July 2, 2026, 5:56am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/8 "2026-07-02T05:56:12Z")

</div>

> [@lukeflo](#):
>
> However, I’m not really sure how/where to track the data progress and, first of all, how to make the `Connection` of `std.http.Client` use my custom implementation. The field [`Connection.stream_reader`](https://ziglang.org/documentation/0.16.0/std/#std.http.Client.Connection) expects type `std.Io.net.Stream.Reader`, how can I put a custom implementation here?

How are you using the HTTP client currently?

It seems that I misremember how the API looks like and it does not actually accept a reader for the body. This is actually better for you, you get a writer, so you can track whatever you are writing on your end.

```zig
  req.transfer_encoding = .{ .content_length = total_len };
  var buffer: [4096]u8 = undefined;
  var body = try req.sendBody(&buffer);
  while (...) |chunk| {
      body.writer.writeAll(chunk) catch |err| switch (err) { // recover error.Canceled }
      // track progress ...
  }   
  try body.end(); 

```

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 2, 2026, 10:15am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/9 "2026-07-02T10:15:54Z")

</div>

Sorry for the delayed answer. Have been busy at my job.

Right now, the http client is used within a lib. The code for executing the request is the following (very simplified):

```zig
// at main.zig create the client and pass it to the underlying library:
    var http_client: std.http.Client = .{
        .allocator = gpa,
        .io = io,
    };

// Inside the library, after some preprocessing of other information, the body is sent as
// follows:
    http_req.transfer_encoding = .{ .content_length = data.content_length };
    var body_writer = http_req.sendBody(&.{}) catch return error.SendFailed;
    body_writer.writer.writeAll(data.body) catch return error.WriteFailed;
    body_writer.end() catch return error.WriteFailed;
    http_req.connection.?.flush() catch return error.FlushFailed

```

If I understand your example code correctly, I just need to use a custom buffer instead of `sendBody(&.{})` and then track the processed chunks inside the `while`-loop which replaces the `body_writer.writer.writeAll(data.body)` single line, sending the value “async” to some kind of shared variable. That would mean I have to edit the lib code, but thats no big deal. What would be the correct `while (<condition>)` in such a case?

Thanks again!

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [July 2, 2026, 10:20am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/10 "2026-07-02T10:20:52Z")

</div>

Wait, you have the entire multi-GiB file allocated into a single buffer? 🙂

The only change then would be to keep what you have, including the empty writer buffer, but instead of `writeAll(data.body)`, you split that into e.g. 1MiB chunks, and track progress after each chunk. Simply have offset as a var, and loop until offset is above the total size.

You are also ignring `error.Canceled` by those catches, but you might not care about that.

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 2, 2026, 10:22am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/11 "2026-07-02T10:22:21Z")

</div>

No, I don’t have large files in a single buffer. If the file is larger than e.g. 50MiB, it would be send in multiple requests and merged together on the storage backend again.

Sorry, I missed to add this info to the question. But some users might have slow internet connections and also want to track progress for files smaller than 50MiB or more detailed for larger files than only counting the large chunks (I hope you get what I mean, my English is not the best, especially when typed fast…)

---

<div class="post-metadata">

**Author:** ![lalinsky](https://ziggit.dev/user_avatar/ziggit.dev/lalinsky/32/3720_2.png) [@lalinsky](https://ziggit.dev/u/lalinsky)\
**Post date:** [July 2, 2026, 10:29am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/12 "2026-07-02T10:29:26Z")

</div>

Ok, makes sense, but the approach is the same, you only track progress for each of those 50MiB chunks:

```zig
var offset: usize = 0;
const chunk_size = 1024 * 1024;
while (true) {
   const slice_end = @min(offset + chunk_size, data.body.len);
   const slice = data.body[offset:slice_end];
   try body_writer.writer.writeAll(slice);
   // record progress, you successfully sent slice.len
   if (slice_end < data.body.len) offset = slice_end else break;
}

```

(probably can be written using nicer conditions)

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 2, 2026, 10:33am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/13 "2026-07-02T10:33:10Z")

</div>

Ok thank you very much. I’ll try to implement it test-wise and report if it works; or ask what was wrong 😉

---

<div class="post-metadata">

**Author:** ![xeubie](https://ziggit.dev/user_avatar/ziggit.dev/xeubie/32/4715_2.png) [@xeubie](https://ziggit.dev/u/xeubie)\
**Post date:** [July 2, 2026, 11:50am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/14 "2026-07-02T11:50:09Z")

</div>

```zig
/// wraps a `*std.Io.Reader` and tracks the total number of bytes consumed,
/// providing a `logicalPos` method analogous to `std.Io.File.Reader.logicalPos`.
pub const CountingReader = struct {
    inner: *std.Io.Reader,
    pos: u64 = 0,
    interface: std.Io.Reader,

    pub fn init(inner: *std.Io.Reader, buffer: []u8) CountingReader {
        return .{
            .inner = inner,
            .interface = .{
                .vtable = &.{
                    .stream = streamFn,
                    .discard = discardFn,
                },
                .buffer = buffer,
                .seek = 0,
                .end = 0,
            },
        };
    }

    fn streamFn(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
        const self: *CountingReader = @fieldParentPtr("interface", r);
        const n = try self.inner.stream(w, limit);
        self.pos += n;
        return n;
    }

    fn discardFn(r: *std.Io.Reader, limit: std.Io.Limit) std.Io.Reader.Error!usize {
        const self: *CountingReader = @fieldParentPtr("interface", r);
        const n = try self.inner.discard(limit);
        self.pos += n;
        return n;
    }

    pub fn logicalPos(self: *const CountingReader) u64 {
        return self.pos - self.interface.bufferedLen();
    }
};

```

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 2, 2026, 12:03pm UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/15 "2026-07-02T12:03:01Z")

</div>

Looks very elaborated, thank you very much. I’m learning a lot here.

But where/how would I “insert” your `CountingReader` into the `http.Client`? As value for [`std.http.Client.reader`](https://ziglang.org/documentation/0.16.0/std/#std.http.Client.Request) which is a [`std.http.Reader`](https://ziglang.org/documentation/0.16.0/std/#std.http.Reader) type? Thats where I’m very unsure, so many `Reader`’s in the whole namespace 😄

---

<div class="post-metadata">

**Author:** ![psznm](https://ziggit.dev/user_avatar/ziggit.dev/psznm/32/8377_2.png) [@psznm](https://ziggit.dev/u/psznm)\
**Post date:** [July 2, 2026, 12:36pm UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/16 "2026-07-02T12:36:40Z")

</div>

> [@lukeflo](#):
>
> ```zig
> var body_writer = http_req.sendBody(&.{}) catch return error.SendFailed;
> body_writer.writer.writeAll(data.body) catch return error.WriteFailed;
> 
> ```

You would want to wrap the Writer, not the Reader, but the implementation would be very similiar to the Reader provided by xeubie. Then you would probably change this code to something like

```zig
    var body_writer = http_req.sendBody(&.{}) catch return error.SendFailed;
    var writer_counter: CountingWriter = .init(&body_writer.writer, &.{});
    writer_counter.interface.writeAll(data.body) catch return error.WriteFailed;

```

You could even keep the writer\_counter and just switch the underlying writer, so that it keeps the count, eg.

```zig
    var writer_counter: CountingWriter = .init(undefined, &.{});
    while (chunks.next()) |*data| {
        var body_writer = http_req.sendBody(&.{}) catch return error.SendFailed;
        writer_counter.setWriter(&body_writer.writer);
        writer_counter.interface.writeAll(data.body) catch return error.WriteFailed;

```

But of course, it depends how you will be accessing the count, if it will be different thread/fiber then this is fine, but if you want it on the same thread then you would probably need to run additional logic on each write - that could even be implemented in the `CountingWriter`

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 2, 2026, 12:44pm UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/17 "2026-07-02T12:44:48Z")

</div>

Thanks for the clarification. I’ll try this one out!

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 3, 2026, 8:31am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/18 "2026-07-03T08:31:34Z")

</div>

For my first test I implemented @lalinsky simple solution with simple print statements for now:

```zig
        var body_writer = http_req.sendBody(&.{}) catch return error.SendFailed;
        var offset: usize = 0;
        const chunk_size = 1024;
        while (true) {
            const slice_end = @min(offset + chunk_size, req.body.len);
            const slice = req.body[offset..slice_end];
            try body_writer.writer.writeAll(slice);
            std.debug.print("\r{[percentage]d:.1}%", .{
                .percentage = (@as(f64, @floatFromInt(slice_end)) / @as(f64, @floatFromInt(req.body.len))) * 100,
            });
            if (slice_end < req.body.len) offset = slice_end else break;
        }
        std.debug.print("\n", .{});
        // body_writer.writer.writeAll(req.body) catch return error.WriteFailed;
        body_writer.end() catch return error.WriteFailed;
        http_req.connection.?.flush() catch return error.FlushFailed;

```

So far, this works without problems. Of course, its a very simplified version which can’t stand like this for production code. Nevertheless, it still beats my implementation using the Rust AWS SDK performance-wise (which ofc is not much conntected to progress tracking, but still makes me happy)

@psznm would appreciate it very much if you could explain why to wrap the writer and not the reader as suggestes by @xeubie . This would help me understand those things better. For now, I did not get a cutom writer to work…

---

<div class="post-metadata">

**Author:** ![psznm](https://ziggit.dev/user_avatar/ziggit.dev/psznm/32/8377_2.png) [@psznm](https://ziggit.dev/u/psznm)\
**Post date:** [July 3, 2026, 9:46am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/19 "2026-07-03T09:46:30Z")

</div>

> [@lukeflo](#):
>
> why to wrap the writer and not the reader

Well, because you are writing, not reading. Wrapping reader wouldn’t be of much use to you, since there is no reader to wrap.

If you wrapped the writer and had the debug print logic in it, you could avoid manually splitting the chunks yourself, and have the informational logic separated. The wrapper would cleanly only do informational logic+wrapping, the Io.Reader interface would do the splitting, and this little code slice would just be clean `.writeAll(` call on the wrapper.

---

<div class="post-metadata">

**Author:** ![lukeflo](https://ziggit.dev/user_avatar/ziggit.dev/lukeflo/32/7514_2.png) [@lukeflo](https://ziggit.dev/u/lukeflo)\
**Post date:** [July 3, 2026, 10:53am UTC](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445/20 "2026-07-03T10:53:50Z")

</div>

> [@psznm](#):
>
> Well, because you are writing, not reading. Wrapping reader wouldn’t be of much use to you, since there is no reader to wrap.

Yes, you’re right, thats obvious. My question wasn’t well phrased, sorry for that.

Checking @xeubie 's example and reading the implementation of `std.Io.Writer`, I came up with that wrapping `CountingWriter`:

```zig
pub const CountingWriter = struct {
    inner: *std.Io.Writer,
    pos: u64 = 0,
    interface: std.Io.Writer,

    pub fn init(inner: *std.Io.Writer, buffer: []u8) CountingWriter {
        return .{
            .inner = inner,
            .interface = .{
                .vtable = &.{
                    .drain = drainFn,
                },
                .buffer = buffer,
                .end = 0,
            },
        };
    }

    fn drainFn(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize {
        const self: *CountingWriter = @fieldParentPtr("interface", w);
        const n = try self.inner.vtable.drain(w, data, splat);
        std.debug.print("Drained {d}\n", .{n}); // Might print a lot, but its only for debugging
        self.pos += n;
        return n;
    }

    pub fn logicalPos(self: *const CountingWriter) u64 {
        return self.pos - self.interface.buffered();
    }
};

```

I tried to apply it with the following code (taken from your example):

```zig
var body_writer = http_req.sendBody(&.{}) catch return error.SendFailed;
var writer_counter: CountingWriter = .init(&body_writer.writer, &.{});
writer_counter.interface.writeAll(req.body) catch return error.WriteFailed;
body_writer.end() catch return error.WriteFailed;

```

It compiles, but when I run it with my test bin (which worked with the `while`-loop implementation), it segfaults:

```zig
Segmentation fault at address 0x10
/home/lukeflo/.config/iguana/ver/0.16.0/lib/std/Io/Writer.zig:225:51: 0x15f999f in writeSplatHeaderLimit (std.zig)
        const copy_len = @min(header.len, w.buffer.len - w.end, remaining);
                                                  ^
/home/lukeflo/.config/iguana/ver/0.16.0/lib/std/Io/Writer.zig:212:33: 0x15f95b4 in writeSplatHeader (std.zig)
    return writeSplatHeaderLimit(w, header, data, splat, .unlimited);
                                ^
/home/lukeflo/.config/iguana/ver/0.16.0/lib/std/http.zig:888:43: 0x15f970e in contentLengthDrain (std.zig)
        const n = try out.writeSplatHeader(w.buffered(), data, splat);
                                          ^
/home/lukeflo/Documents/coding/zig/z3/src/s3_request.zig:361:46: 0x15f7c88 in drainFn (root.zig)
        const n = try self.inner.vtable.drain(w, data, splat);
                                             ^
/home/lukeflo/.config/iguana/ver/0.16.0/lib/std/Io/Writer.zig:539:26: 0x1064e2f in write (std.zig)
    return w.vtable.drain(w, &.{bytes}, 1);
                         ^
/home/lukeflo/.config/iguana/ver/0.16.0/lib/std/Io/Writer.zig:551:51: 0x115926a in writeAll (std.zig)
    while (index < bytes.len) index += try w.write(bytes[index..]);
                                                  ^
/home/lukeflo/Documents/coding/zig/z3/src/s3_request.zig:297:42: 0x122f32a in executeRequest (root.zig)
        writer_counter.interface.writeAll(req.body) catch return error.WriteFailed;
                                         ^
/home/lukeflo/Documents/coding/zig/z3/src/s3_ops.zig:121:34: 0x1230cb8 in putObject (root.zig)
    return request.executeRequest(self, &req);
                                 ^
/home/lukeflo/Documents/coding/zig/z3/examples/testtt.zig:24:36: 0x1207cf0 in main (testtt.zig)
    var resp = try client.putObject("zigtest", "text", file, .{});
                                   ^
/home/lukeflo/.config/iguana/ver/0.16.0/lib/std/start.zig:737:30: 0x1208a8e in callMain (std.zig)
    return wrapMain(root.main(.{
                             ^
/home/lukeflo/.config/iguana/ver/0.16.0/lib/std/start.zig:190:5: 0x1207201 in _start (std.zig)
    asm volatile (switch (native_arch) {

```

Maybe I need to wrap more of the `Writer.vtable` functions, but not sure, since beside `drain()` all have a default.

It likely the mistake I made is obvious, but not for me at the moment…

I’ll keep on with debugging/testing, but if you or anyone knows from the error stack what went wrong, I’ll be thankful for that 🙂

[Next page](https://ziggit.dev/t/track-transferred-bytes-progress-of-longer-put-request-with-std-http/16445.md?page=2)
