diff --git a/examples/gateway/car/main.go b/examples/gateway/car/main.go
index 070dacb94..b0884e1f2 100644
--- a/examples/gateway/car/main.go
+++ b/examples/gateway/car/main.go
@@ -14,6 +14,8 @@ import (
 	"github.com/ipfs/go-libipfs/examples/gateway/common"
 	"github.com/ipfs/go-libipfs/gateway"
 	carblockstore "github.com/ipld/go-car/v2/blockstore"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
 
 func main() {
@@ -52,9 +54,23 @@ func main() {
 			UseSubdomains: true,
 		},
 	}
-	handler = gateway.WithHostname(handler, gwAPI, publicGateways, noDNSLink)
+
+	// Creates a mux to serve the prometheus metrics alongside the gateway. This
+	// step is optional and only required if you need or want to access the metrics.
+	// You may also decide to expose the metrics on a different path, or port.
+	mux := http.NewServeMux()
+	mux.Handle("/debug/metrics/prometheus", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}))
+	mux.Handle("/", handler)
+
+	// Then wrap the mux with the hostname handler. Please note that the metrics
+	// will not be available under the previously defined publicGateways.
+	// You will be able to access the metrics via 127.0.0.1 but not localhost
+	// or example.net. If you want to expose the metrics on such gateways,
+	// you will have to add the path "/debug" to the variable Paths.
+	handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink)
 
 	log.Printf("Listening on http://localhost:%d", *port)
+	log.Printf("Metrics available at http://127.0.0.1:%d/debug/metrics/prometheus", *port)
 	for _, cid := range roots {
 		log.Printf("Hosting CAR root at http://localhost:%d/ipfs/%s", *port, cid.String())
 	}
diff --git a/examples/gateway/proxy/main.go b/examples/gateway/proxy/main.go
index 6f9282c19..793fee121 100644
--- a/examples/gateway/proxy/main.go
+++ b/examples/gateway/proxy/main.go
@@ -10,6 +10,8 @@ import (
 	offline "github.com/ipfs/go-ipfs-exchange-offline"
 	"github.com/ipfs/go-libipfs/examples/gateway/common"
 	"github.com/ipfs/go-libipfs/gateway"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
 
 func main() {
@@ -50,9 +52,25 @@ func main() {
 			UseSubdomains: true,
 		},
 	}
-	handler = gateway.WithHostname(handler, gwAPI, publicGateways, noDNSLink)
+
+	// Creates a mux to serve the prometheus metrics alongside the gateway. This
+	// step is optional and only required if you need or want to access the metrics.
+	// You may also decide to expose the metrics on a different path, or port.
+	mux := http.NewServeMux()
+	mux.Handle("/debug/metrics/prometheus", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}))
+	mux.Handle("/", handler)
+
+	// Then wrap the mux with the hostname handler. Please note that the metrics
+	// will not be available under the previously defined publicGateways.
+	// You will be able to access the metrics via 127.0.0.1 but not localhost
+	// or example.net. If you want to expose the metrics on such gateways,
+	// you will have to add the path "/debug" to the variable Paths.
+	handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink)
 
 	log.Printf("Listening on http://localhost:%d", *port)
+	log.Printf("Try loading an image: http://localhost:%d/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", *port)
+	log.Printf("Try browsing Wikipedia snapshot: http://localhost:%d/ipfs/bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze", *port)
+	log.Printf("Metrics available at http://127.0.0.1:%d/debug/metrics/prometheus", *port)
 	if err := http.ListenAndServe(":"+strconv.Itoa(*port), handler); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/gateway/proxy/routing.go b/examples/gateway/proxy/routing.go
index 24a6903ae..57d0c1492 100644
--- a/examples/gateway/proxy/routing.go
+++ b/examples/gateway/proxy/routing.go
@@ -89,7 +89,6 @@ func (ps *proxyRouting) fetch(ctx context.Context, id peer.ID) ([]byte, error) {
 		},
 	})
 	if err != nil {
-		fmt.Println(err)
 		return nil, err
 	}
 	defer resp.Body.Close()
diff --git a/examples/go.mod b/examples/go.mod
index e4f837ad2..b6d0b1372 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/ipld/go-codec-dagpb v1.5.0
 	github.com/ipld/go-ipld-prime v0.19.0
 	github.com/libp2p/go-libp2p v0.24.2
+	github.com/prometheus/client_golang v1.14.0
 	github.com/stretchr/testify v1.8.1
 )
 
@@ -89,7 +90,6 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/polydawn/refmt v0.89.0 // indirect
-	github.com/prometheus/client_golang v1.14.0 // indirect
 	github.com/prometheus/client_model v0.3.0 // indirect
 	github.com/prometheus/common v0.37.0 // indirect
 	github.com/prometheus/procfs v0.8.0 // indirect
diff --git a/gateway/handler.go b/gateway/handler.go
index 50f7aabc6..a4716e2db 100644
--- a/gateway/handler.go
+++ b/gateway/handler.go
@@ -74,10 +74,14 @@ type handler struct {
 	unixfsGetMetric            *prometheus.SummaryVec // deprecated, use firstContentBlockGetMetric
 
 	// response type metrics
-	unixfsFileGetMetric   *prometheus.HistogramVec
-	unixfsGenDirGetMetric *prometheus.HistogramVec
-	carStreamGetMetric    *prometheus.HistogramVec
-	rawBlockGetMetric     *prometheus.HistogramVec
+	getMetric                 *prometheus.HistogramVec
+	unixfsFileGetMetric       *prometheus.HistogramVec
+	unixfsGenDirGetMetric     *prometheus.HistogramVec
+	carStreamGetMetric        *prometheus.HistogramVec
+	rawBlockGetMetric         *prometheus.HistogramVec
+	tarStreamGetMetric        *prometheus.HistogramVec
+	jsoncborDocumentGetMetric *prometheus.HistogramVec
+	ipnsRecordGetMetric       *prometheus.HistogramVec
 }
 
 // StatusResponseWriter enables us to override HTTP Status Code passed to
@@ -232,6 +236,11 @@ func newHandler(c Config, api API) *handler {
 
 		// Response-type specific metrics
 		// ----------------------------
+		// Generic: time it takes to execute a successful gateway request (all request types)
+		getMetric: newHistogramMetric(
+			"gw_get_duration_seconds",
+			"The time to GET a successful response to a request (all content types).",
+		),
 		// UnixFS: time it takes to return a file
 		unixfsFileGetMetric: newHistogramMetric(
 			"gw_unixfs_file_get_duration_seconds",
@@ -252,13 +261,28 @@ func newHandler(c Config, api API) *handler {
 			"gw_raw_block_get_duration_seconds",
 			"The time to GET an entire raw Block from the gateway.",
 		),
+		// TAR: time it takes to return requested TAR stream
+		tarStreamGetMetric: newHistogramMetric(
+			"gw_tar_stream_get_duration_seconds",
+			"The time to GET an entire TAR stream from the gateway.",
+		),
+		// JSON/CBOR: time it takes to return requested DAG-JSON/-CBOR document
+		jsoncborDocumentGetMetric: newHistogramMetric(
+			"gw_jsoncbor_get_duration_seconds",
+			"The time to GET an entire DAG-JSON/CBOR block from the gateway.",
+		),
+		// IPNS Record: time it takes to return IPNS record
+		ipnsRecordGetMetric: newHistogramMetric(
+			"gw_ipns_record_get_duration_seconds",
+			"The time to GET an entire IPNS Record from the gateway.",
+		),
 
 		// Legacy Metrics
 		// ----------------------------
 		unixfsGetMetric: newSummaryMetric( // TODO: remove?
 			// (deprecated, use firstContentBlockGetMetric instead)
 			"unixfs_get_latency_seconds",
-			"The time to receive the first UnixFS node on a GET from the gateway.",
+			"DEPRECATED: does not do what you think, use gw_first_content_block_get_latency_seconds instead.",
 		),
 	}
 	return i
@@ -372,43 +396,44 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	var success bool
+
 	// Support custom response formats passed via ?format or Accept HTTP header
 	switch responseFormat {
 	case "", "application/json", "application/cbor":
 		switch mc.Code(resolvedPath.Cid().Prefix().Codec) {
 		case mc.Json, mc.DagJson, mc.Cbor, mc.DagCbor:
 			logger.Debugw("serving codec", "path", contentPath)
-			i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
+			success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
 		default:
 			logger.Debugw("serving unixfs", "path", contentPath)
-			i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
+			success = i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
 		}
-		return
 	case "application/vnd.ipld.raw":
 		logger.Debugw("serving raw block", "path", contentPath)
-		i.serveRawBlock(r.Context(), w, r, resolvedPath, contentPath, begin)
-		return
+		success = i.serveRawBlock(r.Context(), w, r, resolvedPath, contentPath, begin)
 	case "application/vnd.ipld.car":
 		logger.Debugw("serving car stream", "path", contentPath)
 		carVersion := formatParams["version"]
-		i.serveCAR(r.Context(), w, r, resolvedPath, contentPath, carVersion, begin)
-		return
+		success = i.serveCAR(r.Context(), w, r, resolvedPath, contentPath, carVersion, begin)
 	case "application/x-tar":
 		logger.Debugw("serving tar file", "path", contentPath)
-		i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
-		return
+		success = i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
 	case "application/vnd.ipld.dag-json", "application/vnd.ipld.dag-cbor":
 		logger.Debugw("serving codec", "path", contentPath)
-		i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
+		success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
 	case "application/vnd.ipfs.ipns-record":
 		logger.Debugw("serving ipns record", "path", contentPath)
-		i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
-		return
+		success = i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
 	default: // catch-all for unsuported application/vnd.*
 		err := fmt.Errorf("unsupported format %q", responseFormat)
 		webError(w, "failed to respond with requested content type", err, http.StatusBadRequest)
 		return
 	}
+
+	if success {
+		i.getMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+	}
 }
 
 func (i *handler) addUserHeaders(w http.ResponseWriter) {
diff --git a/gateway/handler_block.go b/gateway/handler_block.go
index d9e1b137b..fcb2408bc 100644
--- a/gateway/handler_block.go
+++ b/gateway/handler_block.go
@@ -12,14 +12,15 @@ import (
 )
 
 // serveRawBlock returns bytes behind a raw block
-func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) {
+func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) bool {
 	ctx, span := spanTrace(ctx, "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
 	defer span.End()
+
 	blockCid := resolvedPath.Cid()
 	block, err := i.api.GetBlock(ctx, blockCid)
 	if err != nil {
 		webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 	content := bytes.NewReader(block.RawData())
 
@@ -45,4 +46,6 @@ func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *h
 		// Update metrics
 		i.rawBlockGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
 	}
+
+	return dataSent
 }
diff --git a/gateway/handler_car.go b/gateway/handler_car.go
index b52b113ac..b900d699a 100644
--- a/gateway/handler_car.go
+++ b/gateway/handler_car.go
@@ -16,9 +16,10 @@ import (
 )
 
 // serveCAR returns a CAR stream for specific DAG+selector
-func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) {
+func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) bool {
 	ctx, span := spanTrace(ctx, "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
 	defer span.End()
+
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
@@ -28,7 +29,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
 	default:
 		err := fmt.Errorf("only version=1 is supported")
 		webError(w, "unsupported CAR version", err, http.StatusBadRequest)
-		return
+		return false
 	}
 	rootCid := resolvedPath.Cid()
 
@@ -55,7 +56,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
 	// Finish early if Etag match
 	if r.Header.Get("If-None-Match") == etag {
 		w.WriteHeader(http.StatusNotModified)
-		return
+		return false
 	}
 
 	// Make it clear we don't support range-requests over a car stream
@@ -79,11 +80,12 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R
 		// Due to this, we suggest client always verify that
 		// the received CAR stream response is matching requested DAG selector
 		w.Header().Set("X-Stream-Error", err.Error())
-		return
+		return false
 	}
 
 	// Update metrics
 	i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+	return true
 }
 
 // FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315
diff --git a/gateway/handler_codec.go b/gateway/handler_codec.go
index ba981cd02..9959ddffb 100644
--- a/gateway/handler_codec.go
+++ b/gateway/handler_codec.go
@@ -51,7 +51,7 @@ var contentTypeToExtension = map[string]string{
 	"application/vnd.ipld.dag-cbor": ".cbor",
 }
 
-func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) {
+func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) bool {
 	ctx, span := spanTrace(ctx, "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType)))
 	defer span.End()
 
@@ -65,7 +65,7 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http
 		path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder())
 		err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path)
 		webError(w, "unsupported pathing", err, http.StatusNotImplemented)
-		return
+		return false
 	}
 
 	// If no explicit content type was requested, the response will have one based on the codec from the CID
@@ -75,7 +75,7 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http
 			// Should not happen unless function is called with wrong parameters.
 			err := fmt.Errorf("content type not found for codec: %v", cidCodec)
 			webError(w, "internal error", err, http.StatusInternalServerError)
-			return
+			return false
 		}
 		responseContentType = cidContentType
 	}
@@ -94,14 +94,12 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http
 		download := r.URL.Query().Get("download") == "true"
 
 		if isDAG && acceptsHTML && !download {
-			i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath)
+			return i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath)
 		} else {
 			// This covers CIDs with codec 'json' and 'cbor' as those do not have
 			// an explicit requested content type.
-			i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
+			return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin)
 		}
-
-		return
 	}
 
 	// If DAG-JSON or DAG-CBOR was requested using corresponding plain content type
@@ -110,8 +108,7 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http
 	if ok {
 		for _, skipCodec := range skipCodecs {
 			if skipCodec == cidCodec {
-				i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime)
-				return
+				return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin)
 			}
 		}
 	}
@@ -123,14 +120,14 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http
 		// This is never supposed to happen unless function is called with wrong parameters.
 		err := fmt.Errorf("unsupported content type: %s", requestedContentType)
 		webError(w, err.Error(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	// This handles DAG-* conversions and validations.
-	i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime)
+	return i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime, begin)
 }
 
-func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) {
+func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) bool {
 	// A HTML directory index will be presented, be sure to set the correct
 	// type instead of relying on autodetection (which may fail).
 	w.Header().Set("Content-Type", "text/html")
@@ -155,51 +152,61 @@ func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *
 		CodecHex:  fmt.Sprintf("0x%x", uint64(cidCodec)),
 	}); err != nil {
 		webError(w, "failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw", err, http.StatusInternalServerError)
+		return false
 	}
+
+	return true
 }
 
 // serveCodecRaw returns the raw block without any conversion
-func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime time.Time) {
+func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime, begin time.Time) bool {
 	blockCid := resolvedPath.Cid()
 	block, err := i.api.GetBlock(ctx, blockCid)
 	if err != nil {
 		webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 	content := bytes.NewReader(block.RawData())
 
 	// ServeContent will take care of
 	// If-None-Match+Etag, Content-Length and range requests
-	_, _, _ = ServeContent(w, r, name, modtime, content)
+	_, dataSent, _ := ServeContent(w, r, name, modtime, content)
+
+	if dataSent {
+		// Update metrics
+		i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+	}
+
+	return dataSent
 }
 
 // serveCodecConverted returns payload converted to codec specified in toCodec
-func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime time.Time) {
+func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime, begin time.Time) bool {
 	blockCid := resolvedPath.Cid()
 	block, err := i.api.GetBlock(ctx, blockCid)
 	if err != nil {
 		webError(w, "ipfs block get "+html.EscapeString(resolvedPath.String()), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	codec := blockCid.Prefix().Codec
 	decoder, err := multicodec.LookupDecoder(codec)
 	if err != nil {
 		webError(w, err.Error(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	node := basicnode.Prototype.Any.NewBuilder()
 	err = decoder(node, bytes.NewReader(block.RawData()))
 	if err != nil {
 		webError(w, err.Error(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	encoder, err := multicodec.LookupEncoder(uint64(toCodec))
 	if err != nil {
 		webError(w, err.Error(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	// Ensure IPLD node conforms to the codec specification.
@@ -207,7 +214,7 @@ func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter
 	err = encoder(node.Build(), &buf)
 	if err != nil {
 		webError(w, err.Error(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	// Sets correct Last-Modified header. This code is borrowed from the standard
@@ -216,7 +223,14 @@ func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter
 		w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
 	}
 
-	_, _ = w.Write(buf.Bytes())
+	_, err = w.Write(buf.Bytes())
+	if err == nil {
+		// Update metrics
+		i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+		return true
+	}
+
+	return false
 }
 
 func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string {
diff --git a/gateway/handler_ipns_record.go b/gateway/handler_ipns_record.go
index 50d99e231..e2f658579 100644
--- a/gateway/handler_ipns_record.go
+++ b/gateway/handler_ipns_record.go
@@ -12,14 +12,19 @@ import (
 	"github.com/ipfs/go-cid"
 	ipns_pb "github.com/ipfs/go-ipns/pb"
 	ipath "github.com/ipfs/interface-go-ipfs-core/path"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
 	"go.uber.org/zap"
 )
 
-func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) {
+func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool {
+	ctx, span := spanTrace(ctx, "ServeIPNSRecord", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
+	defer span.End()
+
 	if contentPath.Namespace() != "ipns" {
 		err := fmt.Errorf("%s is not an IPNS link", contentPath.String())
 		webError(w, err.Error(), err, http.StatusBadRequest)
-		return
+		return false
 	}
 
 	key := contentPath.String()
@@ -28,26 +33,26 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r
 	if strings.Count(key, "/") != 0 {
 		err := errors.New("cannot find ipns key for subpath")
 		webError(w, err.Error(), err, http.StatusBadRequest)
-		return
+		return false
 	}
 
 	c, err := cid.Decode(key)
 	if err != nil {
 		webError(w, err.Error(), err, http.StatusBadRequest)
-		return
+		return false
 	}
 
 	rawRecord, err := i.api.GetIPNSRecord(ctx, c)
 	if err != nil {
 		webError(w, err.Error(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	var record ipns_pb.IpnsEntry
 	err = proto.Unmarshal(rawRecord, &record)
 	if err != nil {
 		webError(w, err.Error(), err, http.StatusInternalServerError)
-		return
+		return false
 	}
 
 	// Set cache control headers based on the TTL set in the IPNS record. If the
@@ -74,5 +79,12 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r
 	w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record")
 	w.Header().Set("X-Content-Type-Options", "nosniff")
 
-	_, _ = w.Write(rawRecord)
+	_, err = w.Write(rawRecord)
+	if err == nil {
+		// Update metrics
+		i.ipnsRecordGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+		return true
+	}
+
+	return false
 }
diff --git a/gateway/handler_tar.go b/gateway/handler_tar.go
index decdf87c9..9c7026d33 100644
--- a/gateway/handler_tar.go
+++ b/gateway/handler_tar.go
@@ -15,7 +15,7 @@ import (
 
 var unixEpochTime = time.Unix(0, 0)
 
-func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) {
+func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool {
 	ctx, span := spanTrace(ctx, "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
 	defer span.End()
 
@@ -26,7 +26,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R
 	file, err := i.api.GetUnixFsNode(ctx, resolvedPath)
 	if err != nil {
 		webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest)
-		return
+		return false
 	}
 	defer file.Close()
 
@@ -46,7 +46,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R
 	// Finish early if Etag match
 	if r.Header.Get("If-None-Match") == etag {
 		w.WriteHeader(http.StatusNotModified)
-		return
+		return false
 	}
 
 	// Set Content-Disposition
@@ -62,7 +62,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R
 	tarw, err := files.NewTarWriter(w)
 	if err != nil {
 		webError(w, "could not build tar writer", err, http.StatusInternalServerError)
-		return
+		return false
 	}
 	defer tarw.Close()
 
@@ -86,6 +86,10 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R
 		// (1) detect error by having corrupted TAR
 		// (2) be able to reason what went wrong by instecting the tail of TAR stream
 		_, _ = w.Write([]byte(err.Error()))
-		return
+		return false
 	}
+
+	// Update metrics
+	i.tarStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
+	return true
 }
diff --git a/gateway/handler_unixfs.go b/gateway/handler_unixfs.go
index c443e4b32..c8c37ea43 100644
--- a/gateway/handler_unixfs.go
+++ b/gateway/handler_unixfs.go
@@ -14,7 +14,7 @@ import (
 	"go.uber.org/zap"
 )
 
-func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) {
+func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool {
 	ctx, span := spanTrace(ctx, "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
 	defer span.End()
 
@@ -22,7 +22,7 @@ func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *htt
 	dr, err := i.api.GetUnixFsNode(ctx, resolvedPath)
 	if err != nil {
 		webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest)
-		return
+		return false
 	}
 	defer dr.Close()
 
@@ -30,16 +30,17 @@ func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *htt
 	if f, ok := dr.(files.File); ok {
 		logger.Debugw("serving unixfs file", "path", contentPath)
 		i.serveFile(ctx, w, r, resolvedPath, contentPath, f, begin)
-		return
+		return false
 	}
 
 	// Handling Unixfs directory
 	dir, ok := dr.(files.Directory)
 	if !ok {
 		internalWebError(w, fmt.Errorf("unsupported UnixFS type"))
-		return
+		return false
 	}
 
 	logger.Debugw("serving unixfs directory", "path", contentPath)
 	i.serveDirectory(ctx, w, r, resolvedPath, contentPath, dir, begin, logger)
+	return true
 }