diff --git a/mercurial/configitems.py b/mercurial/configitems.py
--- a/mercurial/configitems.py
+++ b/mercurial/configitems.py
@@ -702,6 +702,13 @@ coreconfigitem(
     b'debug.peer-request',
     default=False,
 )
+# If discovery.exchange-heads is False, the discovery will not start with
+# remote head fetching and local head querying.
+coreconfigitem(
+    b'devel',
+    b'discovery.exchange-heads',
+    default=True,
+)
 # If discovery.grow-sample is False, the sample size used in set discovery will
 # not be increased through the process
 coreconfigitem(
diff --git a/mercurial/setdiscovery.py b/mercurial/setdiscovery.py
--- a/mercurial/setdiscovery.py
+++ b/mercurial/setdiscovery.py
@@ -314,6 +314,8 @@ def findcommonheads(
     else:
         ownheads = [rev for rev in cl.headrevs() if rev != nullrev]
 
+    initial_head_exchange = ui.configbool(b'devel', b'discovery.exchange-heads')
+
     # We also ask remote about all the local heads. That set can be arbitrarily
     # large, so we used to limit it size to `initialsamplesize`. We no longer
     # do as it proved counter productive. The skipped heads could lead to a
@@ -365,33 +367,39 @@ def findcommonheads(
     #     graph (with many heads) attached to, but very independant to a the
     #     "simple" graph on the server. This is a fairly usual case and have
     #     not been met in the wild so far.
-    if remote.limitedarguments:
-        sample = _limitsample(ownheads, initialsamplesize)
-        # indices between sample and externalized version must match
-        sample = list(sample)
-    else:
-        sample = ownheads
+    if initial_head_exchange:
+        if remote.limitedarguments:
+            sample = _limitsample(ownheads, initialsamplesize)
+            # indices between sample and externalized version must match
+            sample = list(sample)
+        else:
+            sample = ownheads
 
-    ui.debug(b"query 1; heads\n")
-    roundtrips += 1
-    with remote.commandexecutor() as e:
-        fheads = e.callcommand(b'heads', {})
-        fknown = e.callcommand(
-            b'known',
-            {
-                b'nodes': [clnode(r) for r in sample],
-            },
-        )
+        ui.debug(b"query 1; heads\n")
+        roundtrips += 1
+        with remote.commandexecutor() as e:
+            fheads = e.callcommand(b'heads', {})
+            fknown = e.callcommand(
+                b'known',
+                {
+                    b'nodes': [clnode(r) for r in sample],
+                },
+            )
+
+        srvheadhashes, yesno = fheads.result(), fknown.result()
 
-    srvheadhashes, yesno = fheads.result(), fknown.result()
-
-    if audit is not None:
-        audit[b'total-roundtrips'] = 1
+        if audit is not None:
+            audit[b'total-roundtrips'] = 1
 
-    if cl.tip() == nullid:
-        if srvheadhashes != [nullid]:
-            return [nullid], True, srvheadhashes
-        return [nullid], False, []
+        if cl.tip() == nullid:
+            if srvheadhashes != [nullid]:
+                return [nullid], True, srvheadhashes
+            return [nullid], False, []
+    else:
+        # we still need the remote head for the function return
+        with remote.commandexecutor() as e:
+            fheads = e.callcommand(b'heads', {})
+        srvheadhashes = fheads.result()
 
     # start actual discovery (we note this before the next "if" for
     # compatibility reasons)
@@ -408,15 +416,16 @@ def findcommonheads(
         except error.LookupError:
             continue
 
-    # early exit if we know all the specified remote heads already
-    if len(knownsrvheads) == len(srvheadhashes):
-        ui.debug(b"all remote heads known locally\n")
-        return srvheadhashes, False, srvheadhashes
+    if initial_head_exchange:
+        # early exit if we know all the specified remote heads already
+        if len(knownsrvheads) == len(srvheadhashes):
+            ui.debug(b"all remote heads known locally\n")
+            return srvheadhashes, False, srvheadhashes
 
-    if len(sample) == len(ownheads) and all(yesno):
-        ui.note(_(b"all local changesets known remotely\n"))
-        ownheadhashes = [clnode(r) for r in ownheads]
-        return ownheadhashes, True, srvheadhashes
+        if len(sample) == len(ownheads) and all(yesno):
+            ui.note(_(b"all local changesets known remotely\n"))
+            ownheadhashes = [clnode(r) for r in ownheads]
+            return ownheadhashes, True, srvheadhashes
 
     # full blown discovery
 
@@ -429,12 +438,13 @@ def findcommonheads(
     disco = partialdiscovery(
         local, ownheads, hard_limit_sample, randomize=randomize
     )
-    # treat remote heads (and maybe own heads) as a first implicit sample
-    # response
-    disco.addcommons(knownsrvheads)
-    disco.addinfo(zip(sample, yesno))
+    if initial_head_exchange:
+        # treat remote heads (and maybe own heads) as a first implicit sample
+        # response
+        disco.addcommons(knownsrvheads)
+        disco.addinfo(zip(sample, yesno))
 
-    full = False
+    full = not initial_head_exchange
     progress = ui.makeprogress(_(b'searching'), unit=_(b'queries'))
     while not disco.iscomplete():
 
diff --git a/tests/test-setdiscovery.t b/tests/test-setdiscovery.t
--- a/tests/test-setdiscovery.t
+++ b/tests/test-setdiscovery.t
@@ -1412,23 +1412,22 @@ One with >200 heads. We now switch to se
       missing:                1040
   common heads: 3ee37d65064a
 
-  $ hg -R a debugdiscovery b --debug --config devel.discovery.randomize=false --config devel.discovery.grow-sample.rate=1.01
+  $ hg -R a debugdiscovery b --debug --config devel.discovery.exchange-heads=false --config devel.discovery.randomize=false --config devel.discovery.grow-sample.rate=1.01
   comparing with b
-  query 1; heads
   searching for changes
-  taking quick initial sample
-  query 2; still undecided: 1080, sample size is: 100
   sampling from both directions
-  query 3; still undecided: 980, sample size is: 200
+  query 1; still undecided: 1340, sample size is: 200
+  sampling from both directions
+  query 2; still undecided: 795, sample size is: 202
   sampling from both directions
-  query 4; still undecided: 497, sample size is: 202
+  query 3; still undecided: 525, sample size is: 204
   sampling from both directions
-  query 5; still undecided: 294, sample size is: 204
+  query 4; still undecided: 252, sample size is: 206
   sampling from both directions
-  query 6; still undecided: 90, sample size is: 90
-  6 total queries in *s (glob)
+  query 5; still undecided: 44, sample size is: 44
+  5 total queries in *s (glob)
   elapsed time: * seconds (glob)
-  round-trips:                   6
+  round-trips:                   5
   heads summary:
     total common heads:          1
       also local heads:          0