diff --git a/plugins/analysis/client/fpc_heartbeato.go b/plugins/analysis/client/fpc_heartbeato.go
new file mode 100644
index 0000000000000000000000000000000000000000..1cedde60d07e67715c0c61b2531169d785cf656a
--- /dev/null
+++ b/plugins/analysis/client/fpc_heartbeato.go
@@ -0,0 +1,96 @@
+package client
+
+import (
+	"sync"
+
+	"github.com/iotaledger/goshimmer/packages/metrics"
+	"github.com/iotaledger/goshimmer/packages/vote"
+	"github.com/iotaledger/goshimmer/plugins/analysis/packet"
+	"github.com/iotaledger/goshimmer/plugins/autopeering/local"
+)
+
+var (
+	finalized      map[string]vote.Opinion
+	finalizedMutex sync.RWMutex
+)
+
+func onFinalized(id string, opinion vote.Opinion) {
+	finalizedMutex.Lock()
+	finalized[id] = opinion
+	finalizedMutex.Unlock()
+}
+
+func onRoundExecuted(roundStats *vote.RoundStats) {
+	// get own ID
+	var nodeID []byte
+	if local.GetInstance() != nil {
+		// doesn't copy the ID, take care not to modify underlying bytearray!
+		nodeID = local.GetInstance().ID().Bytes()
+	}
+
+	chunks := splitFPCVoteContext(roundStats.ActiveVoteContexts)
+
+	connLock.Lock()
+	defer connLock.Unlock()
+
+	for _, chunk := range chunks {
+		// abort if empty round
+		if len(chunk) == 0 {
+			return
+		}
+
+		rs := vote.RoundStats{
+			Duration:           roundStats.Duration,
+			RandUsed:           roundStats.RandUsed,
+			ActiveVoteContexts: chunk,
+		}
+
+		hb := &packet.FPCHeartbeat{
+			OwnID:      nodeID,
+			RoundStats: rs,
+		}
+
+		finalizedMutex.Lock()
+		hb.Finalized = finalized
+		finalized = make(map[string]vote.Opinion)
+		finalizedMutex.Unlock()
+
+		data, err := packet.NewFPCHeartbeatMessage(hb)
+		if err != nil {
+			log.Info(err, " - FPC heartbeat message skipped")
+			return
+		}
+
+		log.Info("Client: onRoundExecuted data size: ", len(data))
+
+		if _, err = managedConn.Write(data); err != nil {
+			log.Debugw("Error while writing to connection", "Description", err)
+			return
+		}
+		// trigger AnalysisOutboundBytes event
+		metrics.Events().AnalysisOutboundBytes.Trigger(uint64(len(data)))
+	}
+}
+
+func splitFPCVoteContext(ctx map[string]*vote.Context) (chunk []map[string]*vote.Context) {
+	chunk = make([]map[string]*vote.Context, 1)
+	i, counter := 0, 0
+	chunk[i] = make(map[string]*vote.Context)
+
+	if len(ctx) < maxVoteContext {
+		chunk[i] = ctx
+		return
+	}
+
+	for conflictID, voteCtx := range ctx {
+		counter++
+		if counter >= maxVoteContext {
+			counter = 0
+			i++
+			chunk = append(chunk, make(map[string]*vote.Context))
+		}
+		chunk[i][conflictID] = voteCtx
+	}
+
+	return
+}
diff --git a/plugins/analysis/client/heartbeat.go b/plugins/analysis/client/heartbeat.go
new file mode 100644
index 0000000000000000000000000000000000000000..365562bc4e32170f2527f2c7454e0f95b8e05261
--- /dev/null
+++ b/plugins/analysis/client/heartbeat.go
@@ -0,0 +1,79 @@
+package client
+
+import (
+	"strings"
+
+	"github.com/iotaledger/goshimmer/packages/metrics"
+	"github.com/iotaledger/goshimmer/plugins/analysis/packet"
+	"github.com/iotaledger/goshimmer/plugins/autopeering"
+	"github.com/iotaledger/goshimmer/plugins/autopeering/local"
+	"github.com/iotaledger/hive.go/network"
+	"github.com/mr-tron/base58"
+)
+
+// EventDispatchers holds the Heartbeat function.
+type EventDispatchers struct {
+	// Heartbeat defines the Heartbeat function.
+	Heartbeat func(heartbeat *packet.Heartbeat)
+}
+
+func sendHeartbeat(conn *network.ManagedConnection, hb *packet.Heartbeat) {
+	var out strings.Builder
+	for _, value := range hb.OutboundIDs {
+		out.WriteString(base58.Encode(value))
+	}
+	var in strings.Builder
+	for _, value := range hb.InboundIDs {
+		in.WriteString(base58.Encode(value))
+	}
+	log.Debugw(
+		"Heartbeat",
+		"nodeID", base58.Encode(hb.OwnID),
+		"outboundIDs", out.String(),
+		"inboundIDs", in.String(),
+	)
+
+	data, err := packet.NewHeartbeatMessage(hb)
+	if err != nil {
+		log.Info(err, " - heartbeat message skipped")
+		return
+	}
+
+	connLock.Lock()
+	defer connLock.Unlock()
+	if _, err = conn.Write(data); err != nil {
+		log.Debugw("Error while writing to connection", "Description", err)
+	}
+	// trigger AnalysisOutboundBytes event
+	metrics.Events().AnalysisOutboundBytes.Trigger(uint64(len(data)))
+}
+
+func createHeartbeat() *packet.Heartbeat {
+	// get own ID
+	var nodeID []byte
+	if local.GetInstance() != nil {
+		// doesn't copy the ID, take care not to modify underlying bytearray!
+		nodeID = local.GetInstance().ID().Bytes()
+	}
+
+	var outboundIDs [][]byte
+	var inboundIDs [][]byte
+
+	// get outboundIDs (chosen neighbors)
+	outgoingNeighbors := autopeering.Selection().GetOutgoingNeighbors()
+	outboundIDs = make([][]byte, len(outgoingNeighbors))
+	for i, neighbor := range outgoingNeighbors {
+		// doesn't copy the ID, take care not to modify underlying bytearray!
+		outboundIDs[i] = neighbor.ID().Bytes()
+	}
+
+	// get inboundIDs (accepted neighbors)
+	incomingNeighbors := autopeering.Selection().GetIncomingNeighbors()
+	inboundIDs = make([][]byte, len(incomingNeighbors))
+	for i, neighbor := range incomingNeighbors {
+		// doesn't copy the ID, take care not to modify underlying bytearray!
+		inboundIDs[i] = neighbor.ID().Bytes()
+	}
+
+	return &packet.Heartbeat{OwnID: nodeID, OutboundIDs: outboundIDs, InboundIDs: inboundIDs}
+}
diff --git a/plugins/analysis/client/metric_heartbeat.go b/plugins/analysis/client/metric_heartbeat.go
new file mode 100644
index 0000000000000000000000000000000000000000..46d06ef15db4575824c12322d3d50cff4fdfbaf8
--- /dev/null
+++ b/plugins/analysis/client/metric_heartbeat.go
@@ -0,0 +1,56 @@
+package client
+
+import (
+	"runtime"
+	"time"
+
+	"github.com/iotaledger/goshimmer/packages/metrics"
+	"github.com/iotaledger/goshimmer/plugins/analysis/packet"
+	"github.com/iotaledger/goshimmer/plugins/autopeering/local"
+	"github.com/iotaledger/hive.go/network"
+	"github.com/shirou/gopsutil/cpu"
+)
+
+func sendMetricHeartbeat(conn *network.ManagedConnection, hb *packet.MetricHeartbeat) {
+	data, err := packet.NewMetricHeartbeatMessage(hb)
+	if err != nil {
+		log.Info(err, " - metric heartbeat message skipped")
+		return
+	}
+
+	connLock.Lock()
+	defer connLock.Unlock()
+	if _, err = conn.Write(data); err != nil {
+		log.Debugw("Error while writing to connection", "Description", err)
+	}
+	// trigger AnalysisOutboundBytes event
+	metrics.Events().AnalysisOutboundBytes.Trigger(uint64(len(data)))
+}
+
+func createMetricHeartbeat() *packet.MetricHeartbeat {
+	// get own ID
+	var nodeID []byte
+	if local.GetInstance() != nil {
+		// doesn't copy the ID, take care not to modify underlying bytearray!
+		nodeID = local.GetInstance().ID().Bytes()
+	}
+
+	return &packet.MetricHeartbeat{
+		OwnID:  nodeID,
+		OS:     runtime.GOOS,
+		Arch:   runtime.GOARCH,
+		NumCPU: runtime.NumCPU(),
+		CPUUsage: func() (p float64) {
+			percent, err := cpu.Percent(time.Second, false)
+			if err == nil {
+				p = percent[0]
+			}
+			return
+		}(),
+		MemoryUsage: func() uint64 {
+			var m runtime.MemStats
+			runtime.ReadMemStats(&m)
+			return m.Alloc
+		}(),
+	}
+}
diff --git a/plugins/analysis/client/plugin.go b/plugins/analysis/client/plugin.go
index b710888dd2cf58c146f15e6fc67f7ca85033409a..e79fc801b293b3864392f1dc98baa2d233d18d4d 100644
--- a/plugins/analysis/client/plugin.go
+++ b/plugins/analysis/client/plugin.go
@@ -2,24 +2,18 @@ package client
 
 import (
 	"net"
-	"strings"
 	"sync"
 	"time"
 
 	"github.com/iotaledger/goshimmer/dapps/valuetransfers"
-	"github.com/iotaledger/goshimmer/packages/metrics"
 	"github.com/iotaledger/goshimmer/packages/shutdown"
 	"github.com/iotaledger/goshimmer/packages/vote"
-	"github.com/iotaledger/goshimmer/plugins/analysis/packet"
-	"github.com/iotaledger/goshimmer/plugins/autopeering"
-	"github.com/iotaledger/goshimmer/plugins/autopeering/local"
 	"github.com/iotaledger/goshimmer/plugins/config"
 	"github.com/iotaledger/hive.go/daemon"
 	"github.com/iotaledger/hive.go/events"
 	"github.com/iotaledger/hive.go/logger"
 	"github.com/iotaledger/hive.go/network"
 	"github.com/iotaledger/hive.go/node"
-	"github.com/mr-tron/base58"
 	flag "github.com/spf13/pflag"
 )
 
@@ -44,9 +38,6 @@ var (
 	log         *logger.Logger
 	managedConn *network.ManagedConnection
 	connLock    sync.Mutex
-
-	finalized      map[string]vote.Opinion
-	finalizedMutex sync.RWMutex
 )
 
 func run(_ *node.Plugin) {
@@ -80,157 +71,10 @@ func run(_ *node.Plugin) {
 
 			case <-ticker.C:
 				sendHeartbeat(managedConn, createHeartbeat())
+				sendMetricHeartbeat(managedConn, createMetricHeartbeat())
 			}
 		}
 	}, shutdown.PriorityAnalysis); err != nil {
 		log.Panicf("Failed to start as daemon: %s", err)
 	}
 }
-
-func onFinalized(id string, opinion vote.Opinion) {
-	finalizedMutex.Lock()
-	finalized[id] = opinion
-	finalizedMutex.Unlock()
-}
-
-// EventDispatchers holds the Heartbeat function.
-type EventDispatchers struct {
-	// Heartbeat defines the Heartbeat function.
-	Heartbeat func(heartbeat *packet.Heartbeat)
-}
-
-func sendHeartbeat(conn *network.ManagedConnection, hb *packet.Heartbeat) {
-	var out strings.Builder
-	for _, value := range hb.OutboundIDs {
-		out.WriteString(base58.Encode(value))
-	}
-	var in strings.Builder
-	for _, value := range hb.InboundIDs {
-		in.WriteString(base58.Encode(value))
-	}
-	log.Debugw(
-		"Heartbeat",
-		"nodeID", base58.Encode(hb.OwnID),
-		"outboundIDs", out.String(),
-		"inboundIDs", in.String(),
-	)
-
-	data, err := packet.NewHeartbeatMessage(hb)
-	if err != nil {
-		log.Info(err, " - heartbeat message skipped")
-		return
-	}
-
-	connLock.Lock()
-	defer connLock.Unlock()
-	if _, err = conn.Write(data); err != nil {
-		log.Debugw("Error while writing to connection", "Description", err)
-	}
-	// trigger AnalysisOutboundBytes event
-	metrics.Events().AnalysisOutboundBytes.Trigger(uint64(len(data)))
-}
-
-func createHeartbeat() *packet.Heartbeat {
-	// get own ID
-	var nodeID []byte
-	if local.GetInstance() != nil {
-		// doesn't copy the ID, take care not to modify underlying bytearray!
-		nodeID = local.GetInstance().ID().Bytes()
-	}
-
-	var outboundIDs [][]byte
-	var inboundIDs [][]byte
-
-	// get outboundIDs (chosen neighbors)
-	outgoingNeighbors := autopeering.Selection().GetOutgoingNeighbors()
-	outboundIDs = make([][]byte, len(outgoingNeighbors))
-	for i, neighbor := range outgoingNeighbors {
-		// doesn't copy the ID, take care not to modify underlying bytearray!
-		outboundIDs[i] = neighbor.ID().Bytes()
-	}
-
-	// get inboundIDs (accepted neighbors)
-	incomingNeighbors := autopeering.Selection().GetIncomingNeighbors()
-	inboundIDs = make([][]byte, len(incomingNeighbors))
-	for i, neighbor := range incomingNeighbors {
-		// doesn't copy the ID, take care not to modify underlying bytearray!
-		inboundIDs[i] = neighbor.ID().Bytes()
-	}
-
-	return &packet.Heartbeat{OwnID: nodeID, OutboundIDs: outboundIDs, InboundIDs: inboundIDs}
-}
-
-func onRoundExecuted(roundStats *vote.RoundStats) {
-	// get own ID
-	var nodeID []byte
-	if local.GetInstance() != nil {
-		// doesn't copy the ID, take care not to modify underlying bytearray!
-		nodeID = local.GetInstance().ID().Bytes()
-	}
-
-	chunks := splitFPCVoteContext(roundStats.ActiveVoteContexts)
-
-	connLock.Lock()
-	defer connLock.Unlock()
-
-	for _, chunk := range chunks {
-		// abort if empty round
-		if len(chunk) == 0 {
-			return
-		}
-
-		rs := vote.RoundStats{
-			Duration:           roundStats.Duration,
-			RandUsed:           roundStats.RandUsed,
-			ActiveVoteContexts: chunk,
-		}
-
-		hb := &packet.FPCHeartbeat{
-			OwnID:      nodeID,
-			RoundStats: rs,
-		}
-
-		finalizedMutex.Lock()
-		hb.Finalized = finalized
-		finalized = make(map[string]vote.Opinion)
-		finalizedMutex.Unlock()
-
-		data, err := packet.NewFPCHeartbeatMessage(hb)
-		if err != nil {
-			log.Info(err, " - FPC heartbeat message skipped")
-			return
-		}
-
-		log.Info("Client: onRoundExecuted data size: ", len(data))
-
-		if _, err = managedConn.Write(data); err != nil {
-			log.Debugw("Error while writing to connection", "Description", err)
-			return
-		}
-		// trigger AnalysisOutboundBytes event
-		metrics.Events().AnalysisOutboundBytes.Trigger(uint64(len(data)))
-	}
-}
-
-func splitFPCVoteContext(ctx map[string]*vote.Context) (chunk []map[string]*vote.Context) {
-	chunk = make([]map[string]*vote.Context, 1)
-	i, counter := 0, 0
-	chunk[i] = make(map[string]*vote.Context)
-
-	if len(ctx) < maxVoteContext {
-		chunk[i] = ctx
-		return
-	}
-
-	for conflictID, voteCtx := range ctx {
-		counter++
-		if counter >= maxVoteContext {
-			counter = 0
-			i++
-			chunk = append(chunk, make(map[string]*vote.Context))
-		}
-		chunk[i][conflictID] = voteCtx
-	}
-
-	return
-}
diff --git a/plugins/analysis/packet/packet.go b/plugins/analysis/packet/packet.go
index 442b1a280c5b56190773dba8eac8333880e0fa4a..d95c72d3bd59896597b5000c2e30ee2027127a01 100644
--- a/plugins/analysis/packet/packet.go
+++ b/plugins/analysis/packet/packet.go
@@ -21,6 +21,7 @@ func init() {
 		tlv.HeaderMessageDefinition,
 		HeartbeatMessageDefinition,
 		FPCHeartbeatMessageDefinition,
+		MetricHeartbeatMessageDefinition,
 	}
 	AnalysisMsgRegistry = message.NewRegistry(definitions)
 }
diff --git a/plugins/analysis/server/events.go b/plugins/analysis/server/events.go
index 95006ccd77a3f4098adf7142688252db08c5acf5..75828e1f8de203a8f0d6ff05089a4501b5a6b91a 100644
--- a/plugins/analysis/server/events.go
+++ b/plugins/analysis/server/events.go
@@ -21,6 +21,8 @@ var Events = struct {
 	Heartbeat *events.Event
 	// FPCHeartbeat triggers when an FPC heartbeat has been received.
 	FPCHeartbeat *events.Event
+	// MetricHeartbeat triggers when an MetricHeartbeat heartbeat has been received.
+	MetricHeartbeat *events.Event
 }{
 	events.NewEvent(stringCaller),
 	events.NewEvent(stringCaller),
@@ -29,6 +31,7 @@ var Events = struct {
 	events.NewEvent(errorCaller),
 	events.NewEvent(heartbeatPacketCaller),
 	events.NewEvent(fpcHeartbeatPacketCaller),
+	events.NewEvent(metricHeartbeatPacketCaller),
 }
 
 func stringCaller(handler interface{}, params ...interface{}) {
@@ -50,3 +53,7 @@ func heartbeatPacketCaller(handler interface{}, params ...interface{}) {
 func fpcHeartbeatPacketCaller(handler interface{}, params ...interface{}) {
 	handler.(func(hb *packet.FPCHeartbeat))(params[0].(*packet.FPCHeartbeat))
 }
+
+func metricHeartbeatPacketCaller(handler interface{}, params ...interface{}) {
+	handler.(func(hb *packet.MetricHeartbeat))(params[0].(*packet.MetricHeartbeat))
+}
diff --git a/plugins/analysis/server/plugin.go b/plugins/analysis/server/plugin.go
index fcd6c23298a87a6640a9d3fb1048c5719f6e4d21..30e317a223e9e75949af8727de436c4086b13f17 100644
--- a/plugins/analysis/server/plugin.go
+++ b/plugins/analysis/server/plugin.go
@@ -112,6 +112,9 @@ func wireUp(p *protocol.Protocol) {
 	p.Events.Received[packet.MessageTypeFPCHeartbeat].Attach(events.NewClosure(func(data []byte) {
 		processFPCHeartbeatPacket(data, p)
 	}))
+	p.Events.Received[packet.MessageTypeMetricHeartbeat].Attach(events.NewClosure(func(data []byte) {
+		processMetricHeartbeatPacket(data, p)
+	}))
 }
 
 // processHeartbeatPacket parses the serialized data into a Heartbeat packet and triggers its event
@@ -125,7 +128,7 @@ func processHeartbeatPacket(data []byte, p *protocol.Protocol) {
 	Events.Heartbeat.Trigger(heartbeatPacket)
 }
 
-// processHeartbeatPacket parses the serialized data into a Heartbeat packet and triggers its event
+// processHeartbeatPacket parses the serialized data into a FPC Heartbeat packet and triggers its event
 func processFPCHeartbeatPacket(data []byte, p *protocol.Protocol) {
 	hb, err := packet.ParseFPCHeartbeat(data)
 	if err != nil {
@@ -135,3 +138,14 @@ func processFPCHeartbeatPacket(data []byte, p *protocol.Protocol) {
 	}
 	Events.FPCHeartbeat.Trigger(hb)
 }
+
+// processMetricHeartbeatPacket parses the serialized data into a MEtric Heartbeat packet and triggers its event
+func processMetricHeartbeatPacket(data []byte, p *protocol.Protocol) {
+	hb, err := packet.ParseMetricHeartbeat(data)
+	if err != nil {
+		Events.Error.Trigger(err)
+		p.CloseConnection()
+		return
+	}
+	Events.MetricHeartbeat.Trigger(hb)
+}
diff --git a/plugins/metrics/parameters.go b/plugins/metrics/parameters.go
index 31df451fdf8e2ccaedd2733206201f9f730ebc68..b504f77eb3997f31e65f3f51f3d9e3719766cc5d 100644
--- a/plugins/metrics/parameters.go
+++ b/plugins/metrics/parameters.go
@@ -1,6 +1,10 @@
 package metrics
 
-import "time"
+import (
+	"time"
+
+	flag "github.com/spf13/pflag"
+)
 
 const (
 	// should always be 1 second
@@ -13,3 +17,15 @@ const (
 	MemUsageMeasurementInterval    = 1 * time.Second
 	SyncedMeasurementInterval      = 1 * time.Second
 )
+
+const (
+	// CfgMetricsLocal defines the config flag to enable/disable local metrics.
+	CfgMetricsLocal = "metrics.local"
+	// CfgMetricsGlobal defines the config flag to enable/disable global metrics.
+	CfgMetricsGlobal = "metrics.global"
+)
+
+func init() {
+	flag.Bool(CfgMetricsLocal, true, "include local metrics")
+	flag.Bool(CfgMetricsGlobal, false, "include global metrics")
+}
diff --git a/plugins/metrics/plugin.go b/plugins/metrics/plugin.go
index ca67ba43de29ec721850d4eb721a1c3e5548e2d5..608dd9a68ddf349d0c4d534627d9303e61f23463 100644
--- a/plugins/metrics/plugin.go
+++ b/plugins/metrics/plugin.go
@@ -12,7 +12,9 @@ import (
 	"github.com/iotaledger/goshimmer/packages/metrics"
 	"github.com/iotaledger/goshimmer/packages/shutdown"
 	"github.com/iotaledger/goshimmer/packages/vote"
+	"github.com/iotaledger/goshimmer/plugins/analysis/server"
 	"github.com/iotaledger/goshimmer/plugins/autopeering"
+	"github.com/iotaledger/goshimmer/plugins/config"
 	"github.com/iotaledger/goshimmer/plugins/gossip"
 	"github.com/iotaledger/goshimmer/plugins/messagelayer"
 	"github.com/iotaledger/hive.go/daemon"
@@ -36,6 +38,40 @@ func configure(_ *node.Plugin) {
 
 func run(_ *node.Plugin) {
 
+	if config.Node.GetBool(CfgMetricsLocal) {
+		registerLocalMetrics()
+	}
+
+	// Events from analysis server
+	if config.Node.GetBool(CfgMetricsGlobal) {
+		server.Events.MetricHeartbeat.Attach(onMetricHeartbeatReceived)
+	}
+
+	// create a background worker that update the metrics every second
+	if err := daemon.BackgroundWorker("Metrics Updater", func(shutdownSignal <-chan struct{}) {
+		if config.Node.GetBool(CfgMetricsLocal) {
+			timeutil.Ticker(func() {
+				measureReceivedMPS()
+				measureCPUUsage()
+				measureMemUsage()
+				measureSynced()
+
+				// gossip network traffic
+				g := gossipCurrentTraffic()
+				gossipCurrentRx.Store(uint64(g.BytesRead))
+				gossipCurrentTx.Store(uint64(g.BytesWritten))
+			}, 1*time.Second, shutdownSignal)
+			timeutil.Ticker(measureMPSPerPayload, MPSMeasurementInterval, shutdownSignal)
+			timeutil.Ticker(measureMessageTips, MessageTipsMeasurementInterval, shutdownSignal)
+			timeutil.Ticker(measureReceivedTPS, TPSMeasurementInterval, shutdownSignal)
+			timeutil.Ticker(measureValueTips, ValueTipsMeasurementInterval, shutdownSignal)
+		}
+	}, shutdown.PriorityMetrics); err != nil {
+		log.Panicf("Failed to start as daemon: %s", err)
+	}
+}
+
+func registerLocalMetrics() {
 	//// Events declared in other packages which we want to listen to here ////
 
 	// increase received MPS counter whenever we attached a message
@@ -110,26 +146,4 @@ func run(_ *node.Plugin) {
 	metrics.Events().QueryReplyError.Attach(events.NewClosure(func(ev *metrics.QueryReplyErrorEvent) {
 		processQueryReplyError(ev)
 	}))
-
-	// create a background worker that "measures" the MPS value every second
-	if err := daemon.BackgroundWorker("Metrics Updater", func(shutdownSignal <-chan struct{}) {
-		timeutil.Ticker(func() {
-			measureReceivedMPS()
-			measureCPUUsage()
-			measureMemUsage()
-			measureSynced()
-
-			// gossip network traffic
-			g := gossipCurrentTraffic()
-			gossipCurrentRx.Store(uint64(g.BytesRead))
-			gossipCurrentTx.Store(uint64(g.BytesWritten))
-
-		}, 1*time.Second, shutdownSignal)
-		timeutil.Ticker(measureMPSPerPayload, MPSMeasurementInterval, shutdownSignal)
-		timeutil.Ticker(measureMessageTips, MessageTipsMeasurementInterval, shutdownSignal)
-		timeutil.Ticker(measureReceivedTPS, TPSMeasurementInterval, shutdownSignal)
-		timeutil.Ticker(measureValueTips, ValueTipsMeasurementInterval, shutdownSignal)
-	}, shutdown.PriorityMetrics); err != nil {
-		log.Panicf("Failed to start as daemon: %s", err)
-	}
 }
diff --git a/plugins/metrics/server.go b/plugins/metrics/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..3c3bc378e52e5b571260824db4f735f7155e26fb
--- /dev/null
+++ b/plugins/metrics/server.go
@@ -0,0 +1,57 @@
+package metrics
+
+import (
+	"bytes"
+	"encoding/gob"
+	"sync"
+
+	"github.com/iotaledger/goshimmer/plugins/analysis/packet"
+	"github.com/iotaledger/hive.go/events"
+	"github.com/mr-tron/base58/base58"
+)
+
+type ClientInfo struct {
+	OS          string
+	Arch        string
+	NumCPU      int
+	CPUUsage    float64
+	MemoryUsage uint64
+}
+
+var (
+	clientsMetrics      = make(map[string]ClientInfo)
+	clientsMetricsMutex sync.RWMutex
+)
+
+var onMetricHeartbeatReceived = events.NewClosure(func(hb *packet.MetricHeartbeat) {
+	clientsMetricsMutex.Lock()
+	defer clientsMetricsMutex.Unlock()
+	clientsMetrics[base58.Encode(hb.OwnID)] = ClientInfo{
+		OS:          hb.OS,
+		Arch:        hb.Arch,
+		NumCPU:      hb.NumCPU,
+		CPUUsage:    hb.CPUUsage,
+		MemoryUsage: hb.MemoryUsage,
+	}
+})
+
+func ClientsMetrics() map[string]ClientInfo {
+	clientsMetricsMutex.RLock()
+	defer clientsMetricsMutex.RUnlock()
+
+	var buf bytes.Buffer
+	enc := gob.NewEncoder(&buf)
+	err := enc.Encode(clientsMetrics)
+	if err != nil {
+		return nil
+	}
+
+	dec := gob.NewDecoder(&buf)
+	var copy map[string]ClientInfo
+	err = dec.Decode(&copy)
+	if err != nil {
+		return nil
+	}
+
+	return copy
+}
diff --git a/plugins/prometheus/analysis_server.go b/plugins/prometheus/analysis_server.go
new file mode 100644
index 0000000000000000000000000000000000000000..7b1b4c03ead561149a146a3dee92ae4191b79990
--- /dev/null
+++ b/plugins/prometheus/analysis_server.go
@@ -0,0 +1 @@
+package prometheus
diff --git a/tools/docker-network/docker-compose.yml b/tools/docker-network/docker-compose.yml
index 467efcc792e804322f527cc922e22ccd76523041..6763e418290636db94f0ea3f7fa046d688729969 100644
--- a/tools/docker-network/docker-compose.yml
+++ b/tools/docker-network/docker-compose.yml
@@ -24,7 +24,9 @@ services:
       --analysis.dashboard.bindAddress=0.0.0.0:9000
       --node.enablePlugins=analysis-server,analysis-dashboard
       --analysis.dashboard.dev=false
-      --node.disablePlugins=portcheck,dashboard,analysis-client,gossip,drng,issuer,sync,metrics,messagelayer,valuetransfers,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint
+      --metrics.local=false
+      --metrics.global=true
+      --node.disablePlugins=portcheck,dashboard,analysis-client,gossip,drng,issuer,sync,messagelayer,valuetransfers,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint
     volumes:
       - ./config.docker.json:/tmp/config.json:ro
       - goshimmer-cache:/go