diff --git a/mercurial/hgweb/protocol.py b/mercurial/hgweb/protocol.py
--- a/mercurial/hgweb/protocol.py
+++ b/mercurial/hgweb/protocol.py
@@ -73,21 +73,30 @@ class webproto(wireproto.abstractserverp
         val = self.ui.fout.getvalue()
         self.ui.ferr, self.ui.fout = self.oldio
         return val
+
     def groupchunks(self, fh):
+        def getchunks():
+            while True:
+                chunk = fh.read(32768)
+                if not chunk:
+                    break
+                yield chunk
+
+        return self.compresschunks(getchunks())
+
+    def compresschunks(self, chunks):
         # Don't allow untrusted settings because disabling compression or
         # setting a very high compression level could lead to flooding
         # the server's network or CPU.
         z = zlib.compressobj(self.ui.configint('server', 'zliblevel', -1))
-        while True:
-            chunk = fh.read(32768)
-            if not chunk:
-                break
+        for chunk in chunks:
             data = z.compress(chunk)
             # Not all calls to compress() emit data. It is cheaper to inspect
             # that here than to send it via the generator.
             if data:
                 yield data
         yield z.flush()
+
     def _client(self):
         return 'remote:%s:%s:%s' % (
             self.req.env.get('wsgi.url_scheme') or 'http',
diff --git a/mercurial/sshserver.py b/mercurial/sshserver.py
--- a/mercurial/sshserver.py
+++ b/mercurial/sshserver.py
@@ -71,6 +71,10 @@ class sshserver(wireproto.abstractserver
     def groupchunks(self, fh):
         return iter(lambda: fh.read(4096), '')
 
+    def compresschunks(self, chunks):
+        for chunk in chunks:
+            yield chunk
+
     def sendresponse(self, v):
         self.fout.write("%d\n" % len(v))
         self.fout.write(v)
diff --git a/mercurial/wireproto.py b/mercurial/wireproto.py
--- a/mercurial/wireproto.py
+++ b/mercurial/wireproto.py
@@ -85,6 +85,14 @@ class abstractserverproto(object):
         """
         raise NotImplementedError()
 
+    def compresschunks(self, chunks):
+        """Generator of possible compressed chunks to send to the client.
+
+        This is like ``groupchunks()`` except it accepts a generator as
+        its argument.
+        """
+        raise NotImplementedError()
+
 class remotebatch(peer.batcher):
     '''batches the queued calls; uses as few roundtrips as possible'''
     def __init__(self, remote):
@@ -773,9 +781,7 @@ def getbundle(repo, proto, others):
             return ooberror(bundle2required)
 
     chunks = exchange.getbundlechunks(repo, 'serve', **opts)
-    # TODO avoid util.chunkbuffer() here since it is adding overhead to
-    # what is fundamentally a generator proxying operation.
-    return streamres(proto.groupchunks(util.chunkbuffer(chunks)))
+    return streamres(proto.compresschunks(chunks))
 
 @wireprotocommand('heads')
 def heads(repo, proto):