diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go
index 095f57a8e..bc89efdf3 100644
--- a/control/controlclient/direct.go
+++ b/control/controlclient/direct.go
@@ -593,7 +593,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
 			lastDERPMap = resp.DERPMap
 		}
 		if resp.Debug != nil && resp.Debug.LogHeapPprof {
-			logheap.LogHeap()
+			go logheap.LogHeap(resp.Debug.LogHeapURL)
 		}
 
 		nm := &NetworkMap{
diff --git a/log/logheap/logheap.go b/log/logheap/logheap.go
index a97d7f6b9..af48a1f64 100644
--- a/log/logheap/logheap.go
+++ b/log/logheap/logheap.go
@@ -7,9 +7,9 @@ package logheap
 
 import (
 	"bytes"
-	"encoding/json"
-	"io"
-	"os"
+	"context"
+	"log"
+	"net/http"
 	"runtime"
 	"runtime/pprof"
 	"time"
@@ -17,29 +17,26 @@ import (
 
 // LogHeap writes a JSON logtail record with the base64 heap pprof to
 // os.Stderr.
-func LogHeap() {
-	logHeap(os.Stderr)
-}
-
-type logTail struct {
-	ClientTime string `json:"client_time"`
-}
-
-type pprofRec struct {
-	Heap []byte `json:"heap,omitempty"`
-}
-
-type logLine struct {
-	LogTail logTail  `json:"logtail"`
-	Pprof   pprofRec `json:"pprof"`
-}
-
-func logHeap(w io.Writer) error {
+func LogHeap(postURL string) {
+	if postURL == "" {
+		return
+	}
 	runtime.GC()
 	buf := new(bytes.Buffer)
 	pprof.WriteHeapProfile(buf)
-	return json.NewEncoder(w).Encode(logLine{
-		LogTail: logTail{ClientTime: time.Now().Format(time.RFC3339Nano)},
-		Pprof:   pprofRec{Heap: buf.Bytes()},
-	})
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	req, err := http.NewRequestWithContext(ctx, "POST", postURL, buf)
+	if err != nil {
+		log.Printf("LogHeap: %v", err)
+		return
+	}
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		log.Printf("LogHeap: %v", err)
+		return
+	}
+	defer res.Body.Close()
+	return
 }
diff --git a/log/logheap/logheap_test.go b/log/logheap/logheap_test.go
deleted file mode 100644
index d55cb5d35..000000000
--- a/log/logheap/logheap_test.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package logheap
-
-import (
-	"bytes"
-	"compress/gzip"
-	"encoding/json"
-	"io/ioutil"
-	"testing"
-)
-
-func TestLogHeap(t *testing.T) {
-	var buf bytes.Buffer
-	if err := logHeap(&buf); err != nil {
-		t.Fatal(err)
-	}
-	t.Logf("Got line: %s", buf.Bytes())
-
-	var ll logLine
-	if err := json.Unmarshal(buf.Bytes(), &ll); err != nil {
-		t.Fatal(err)
-	}
-
-	zr, err := gzip.NewReader(bytes.NewReader(ll.Pprof.Heap))
-	if err != nil {
-		t.Fatal(err)
-	}
-	rawProto, err := ioutil.ReadAll(zr)
-	if err != nil {
-		t.Fatal(err)
-	}
-	// Just sanity check it. Too lazy to properly decode the protobuf. But see that
-	// it contains an expected sample name.
-	if !bytes.Contains(rawProto, []byte("alloc_objects")) {
-		t.Errorf("raw proto didn't contain `alloc_objects`: %q", rawProto)
-	}
-}
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index 0a9ac18eb..3bce4d8e1 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -509,10 +509,14 @@ type MapResponse struct {
 // Debug are instructions from the control server to the client
 // to adjust debug settings.
 type Debug struct {
-	// LogHeapPprof controls whether the client should logs
+	// LogHeapPprof controls whether the client should log
 	// its heap pprof data. Each true value sent from the server
 	// means that client should do one more log.
 	LogHeapPprof bool `json:",omitempty"`
+
+	// LogHeapURL is the URL to POST its heap pprof to.
+	// Empty means to not log.
+	LogHeapURL string `json:",omitempty"`
 }
 
 func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }