diff --git a/node/IncomingPacket.cpp b/node/IncomingPacket.cpp
index a120a208a..12534117f 100644
--- a/node/IncomingPacket.cpp
+++ b/node/IncomingPacket.cpp
@@ -37,6 +37,7 @@
 #include "Trace.hpp"
 #include "Path.hpp"
 #include "Bond.hpp"
+#include "Metrics.hpp"
 
 namespace ZeroTier {
 
@@ -130,6 +131,8 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 	const Packet::ErrorCode errorCode = (Packet::ErrorCode)(*this)[ZT_PROTO_VERB_ERROR_IDX_ERROR_CODE];
 	uint64_t networkId = 0;
 
+	Metrics::pkt_error++;
+
 	/* Security note: we do not gate doERROR() with expectingReplyTo() to
 	 * avoid having to log every outgoing packet ID. Instead we put the
 	 * logic to determine whether we should consider an ERROR in each
@@ -145,6 +148,7 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 				if ((network)&&(network->controller() == peer->address()))
 					network->setNotFound(tPtr);
 			}
+			Metrics::pkt_error_obj_not_found++;
 			break;
 
 		case Packet::ERROR_UNSUPPORTED_OPERATION:
@@ -156,12 +160,15 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 				if ((network)&&(network->controller() == peer->address()))
 					network->setNotFound(tPtr);
 			}
+			Metrics::pkt_error_unsupported_op++;
 			break;
 
 		case Packet::ERROR_IDENTITY_COLLISION:
 			// FIXME: for federation this will need a payload with a signature or something.
-			if (RR->topology->isUpstream(peer->identity()))
+			if (RR->topology->isUpstream(peer->identity())) {
 				RR->node->postEvent(tPtr,ZT_EVENT_FATAL_ERROR_IDENTITY_COLLISION);
+			}
+			Metrics::pkt_error_identity_collision++;
 			break;
 
 		case Packet::ERROR_NEED_MEMBERSHIP_CERTIFICATE: {
@@ -169,15 +176,19 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 			networkId = at<uint64_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD);
 			const SharedPtr<Network> network(RR->node->network(networkId));
 			const int64_t now = RR->node->now();
-			if ((network)&&(network->config().com))
+			if ((network)&&(network->config().com)) {
 				network->peerRequestedCredentials(tPtr,peer->address(),now);
+			}
+			Metrics::pkt_error_need_membership_cert++;
 		}	break;
 
 		case Packet::ERROR_NETWORK_ACCESS_DENIED_: {
 			// Network controller: network access denied.
 			const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD)));
-			if ((network)&&(network->controller() == peer->address()))
+			if ((network)&&(network->controller() == peer->address())) {
 				network->setAccessDenied(tPtr);
+			}
+			Metrics::pkt_error_network_access_denied++;
 		}	break;
 
 		case Packet::ERROR_UNWANTED_MULTICAST: {
@@ -189,6 +200,7 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 				const MulticastGroup mg(MAC(field(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD + 8,6),6),at<uint32_t>(ZT_PROTO_VERB_ERROR_IDX_PAYLOAD + 14));
 				RR->mc->remove(network->id(),mg,peer->address());
 			}
+			Metrics::pkt_error_unwanted_multicast++;
 		}	break;
 
 		case Packet::ERROR_NETWORK_AUTHENTICATION_REQUIRED: {
@@ -247,6 +259,7 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 					network->setAuthenticationRequired(tPtr, "");
 				}
 			}
+			Metrics::pkt_error_authentication_required++;
 		}	break;
 
 		default: break;
@@ -273,11 +286,13 @@ bool IncomingPacket::_doACK(const RuntimeEnvironment* RR, void* tPtr, const Shar
 		bond->receivedAck(_path, RR->node->now(), Utils::ntoh(ackedBytes));
 	}
 	*/
+	Metrics::pkt_ack++;
 	return true;
 }
 
 bool IncomingPacket::_doQOS_MEASUREMENT(const RuntimeEnvironment* RR, void* tPtr, const SharedPtr<Peer>& peer)
 {
+	Metrics::pkt_qos++;
 	SharedPtr<Bond> bond = peer->bond();
 	if (! bond || ! bond->rateGateQoS(RR->node->now(), _path)) {
 		return true;
@@ -308,6 +323,7 @@ bool IncomingPacket::_doQOS_MEASUREMENT(const RuntimeEnvironment* RR, void* tPtr
 
 bool IncomingPacket::_doHELLO(const RuntimeEnvironment *RR,void *tPtr,const bool alreadyAuthenticated)
 {
+	Metrics::pkt_hello++;
 	const int64_t now = RR->node->now();
 
 	const uint64_t pid = packetId();
@@ -510,6 +526,7 @@ bool IncomingPacket::_doHELLO(const RuntimeEnvironment *RR,void *tPtr,const bool
 
 bool IncomingPacket::_doOK(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_ok++;
 	const Packet::Verb inReVerb = (Packet::Verb)(*this)[ZT_PROTO_VERB_OK_IDX_IN_RE_VERB];
 	const uint64_t inRePacketId = at<uint64_t>(ZT_PROTO_VERB_OK_IDX_IN_RE_PACKET_ID);
 	uint64_t networkId = 0;
@@ -623,8 +640,11 @@ bool IncomingPacket::_doOK(const RuntimeEnvironment *RR,void *tPtr,const SharedP
 
 bool IncomingPacket::_doWHOIS(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
-	if ((!RR->topology->amUpstream())&&(!peer->rateGateInboundWhoisRequest(RR->node->now())))
+	if ((!RR->topology->amUpstream())&&(!peer->rateGateInboundWhoisRequest(RR->node->now()))) {
 		return true;
+	}
+
+	Metrics::pkt_whois++;
 
 	Packet outp(peer->address(),RR->identity.address(),Packet::VERB_OK);
 	outp.append((unsigned char)Packet::VERB_WHOIS);
@@ -658,6 +678,7 @@ bool IncomingPacket::_doWHOIS(const RuntimeEnvironment *RR,void *tPtr,const Shar
 
 bool IncomingPacket::_doRENDEZVOUS(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_rendezvous++;
 	if (RR->topology->isUpstream(peer->identity())) {
 		const Address with(field(ZT_PROTO_VERB_RENDEZVOUS_IDX_ZTADDRESS,ZT_ADDRESS_LENGTH),ZT_ADDRESS_LENGTH);
 		const SharedPtr<Peer> rendezvousWith(RR->topology->getPeer(tPtr,with));
@@ -711,6 +732,7 @@ static bool _ipv6GetPayload(const uint8_t *frameData,unsigned int frameLen,unsig
 
 bool IncomingPacket::_doFRAME(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer,int32_t flowId)
 {
+	Metrics::pkt_frame++;
 	int32_t _flowId = ZT_QOS_NO_FLOW;
 	SharedPtr<Bond> bond = peer->bond();
 	if (bond && bond->flowHashingSupported()) {
@@ -803,6 +825,7 @@ bool IncomingPacket::_doFRAME(const RuntimeEnvironment *RR,void *tPtr,const Shar
 
 bool IncomingPacket::_doEXT_FRAME(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer,int32_t flowId)
 {
+	Metrics::pkt_ext_frame++;
 	const uint64_t nwid = at<uint64_t>(ZT_PROTO_VERB_EXT_FRAME_IDX_NETWORK_ID);
 	const SharedPtr<Network> network(RR->node->network(nwid));
 	if (network) {
@@ -885,6 +908,7 @@ bool IncomingPacket::_doEXT_FRAME(const RuntimeEnvironment *RR,void *tPtr,const
 
 bool IncomingPacket::_doECHO(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_echo++;
 	uint64_t now = RR->node->now();
 	if (!_path->rateGateEchoRequest(now)) {
 		return true;
@@ -907,6 +931,7 @@ bool IncomingPacket::_doECHO(const RuntimeEnvironment *RR,void *tPtr,const Share
 
 bool IncomingPacket::_doMULTICAST_LIKE(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_multicast_like++;
 	const int64_t now = RR->node->now();
 	bool authorized = false;
 	uint64_t lastNwid = 0;
@@ -932,8 +957,10 @@ bool IncomingPacket::_doMULTICAST_LIKE(const RuntimeEnvironment *RR,void *tPtr,c
 
 bool IncomingPacket::_doNETWORK_CREDENTIALS(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
-	if (!peer->rateGateCredentialsReceived(RR->node->now()))
+	Metrics::pkt_network_credentials++;
+	if (!peer->rateGateCredentialsReceived(RR->node->now())) {
 		return true;
+	}
 
 	CertificateOfMembership com;
 	Capability cap;
@@ -1055,6 +1082,7 @@ bool IncomingPacket::_doNETWORK_CREDENTIALS(const RuntimeEnvironment *RR,void *t
 
 bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_network_config_request++;
 	const uint64_t nwid = at<uint64_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST_IDX_NETWORK_ID);
 	const unsigned int hopCount = hops();
 	const uint64_t requestPacketId = packetId();
@@ -1081,6 +1109,7 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,void
 
 bool IncomingPacket::_doNETWORK_CONFIG(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_network_config++;
 	const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PACKET_IDX_PAYLOAD)));
 	if (network) {
 		const uint64_t configUpdateId = network->handleConfigChunk(tPtr,packetId(),source(),*this,ZT_PACKET_IDX_PAYLOAD);
@@ -1104,6 +1133,7 @@ bool IncomingPacket::_doNETWORK_CONFIG(const RuntimeEnvironment *RR,void *tPtr,c
 
 bool IncomingPacket::_doMULTICAST_GATHER(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_multicast_gather++;
 	const uint64_t nwid = at<uint64_t>(ZT_PROTO_VERB_MULTICAST_GATHER_IDX_NETWORK_ID);
 	const unsigned int flags = (*this)[ZT_PROTO_VERB_MULTICAST_GATHER_IDX_FLAGS];
 	const MulticastGroup mg(MAC(field(ZT_PROTO_VERB_MULTICAST_GATHER_IDX_MAC,6),6),at<uint32_t>(ZT_PROTO_VERB_MULTICAST_GATHER_IDX_ADI));
@@ -1144,6 +1174,7 @@ bool IncomingPacket::_doMULTICAST_GATHER(const RuntimeEnvironment *RR,void *tPtr
 
 bool IncomingPacket::_doMULTICAST_FRAME(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_multicast_frame++;
 	const uint64_t nwid = at<uint64_t>(ZT_PROTO_VERB_MULTICAST_FRAME_IDX_NETWORK_ID);
 	const unsigned int flags = (*this)[ZT_PROTO_VERB_MULTICAST_FRAME_IDX_FLAGS];
 
@@ -1244,6 +1275,7 @@ bool IncomingPacket::_doMULTICAST_FRAME(const RuntimeEnvironment *RR,void *tPtr,
 
 bool IncomingPacket::_doPUSH_DIRECT_PATHS(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_push_direct_paths++;
 	const int64_t now = RR->node->now();
 
 	if (!peer->rateGatePushDirectPaths(now)) {
@@ -1306,6 +1338,7 @@ bool IncomingPacket::_doPUSH_DIRECT_PATHS(const RuntimeEnvironment *RR,void *tPt
 
 bool IncomingPacket::_doUSER_MESSAGE(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_user_message++;
 	if (likely(size() >= (ZT_PACKET_IDX_PAYLOAD + 8))) {
 		ZT_UserMessage um;
 		um.origin = peer->address().toInt();
@@ -1322,6 +1355,7 @@ bool IncomingPacket::_doUSER_MESSAGE(const RuntimeEnvironment *RR,void *tPtr,con
 
 bool IncomingPacket::_doREMOTE_TRACE(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_remote_trace++;
 	ZT_RemoteTrace rt;
 	const char *ptr = reinterpret_cast<const char *>(data()) + ZT_PACKET_IDX_PAYLOAD;
 	const char *const eof = reinterpret_cast<const char *>(data()) + size();
@@ -1346,6 +1380,7 @@ bool IncomingPacket::_doREMOTE_TRACE(const RuntimeEnvironment *RR,void *tPtr,con
 
 bool IncomingPacket::_doPATH_NEGOTIATION_REQUEST(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
+	Metrics::pkt_path_negotiation_request++;
 	uint64_t now = RR->node->now();
 	SharedPtr<Bond> bond = peer->bond();
 	if (!bond || !bond->rateGatePathNegotiation(now, _path)) {
diff --git a/node/Metrics.cpp b/node/Metrics.cpp
index a89954f59..8ccef869f 100644
--- a/node/Metrics.cpp
+++ b/node/Metrics.cpp
@@ -22,6 +22,68 @@ namespace prometheus {
 
 namespace ZeroTier {
     namespace Metrics {
+        // Packet Type Counts
+        prometheus::simpleapi::counter_family_t packets
+        { "zt_packet_incoming", "incoming packet type counts"};
+        prometheus::simpleapi::counter_metric_t pkt_error
+        { packets.Add({{"packet_type", "error"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_ack
+        { packets.Add({{"packet_type", "ack"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_qos
+        { packets.Add({{"packet_type", "qos"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_hello
+        { packets.Add({{"packet_type", "hello"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_ok
+        { packets.Add({{"packet_type", "ok"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_whois
+        { packets.Add({{"packet_type", "whois"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_rendezvous
+        { packets.Add({{"packet_type", "rendezvous"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_frame
+        { packets.Add({{"packet_type", "frame"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_ext_frame
+        { packets.Add({{"packet_type", "ext_frame"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_echo
+        { packets.Add({{"packet_type", "echo"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_multicast_like
+        { packets.Add({{"packet_type", "multicast_like"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_network_credentials
+        { packets.Add({{"packet_type", "network_credentials"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_network_config_request
+        { packets.Add({{"packet_type", "network_config_request"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_network_config
+        { packets.Add({{"packet_type", "network_config"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_multicast_gather
+        { packets.Add({{"packet_type", "multicast_gather"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_multicast_frame
+        { packets.Add({{"packet_type", "multicast_frame"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_push_direct_paths
+        { packets.Add({{"packet_type", "push_direct_paths"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_user_message
+        { packets.Add({{"packet_type", "user_message"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_remote_trace
+        { packets.Add({{"packet_type", "remote_trace"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_path_negotiation_request
+        { packets.Add({{"packet_type", "path_negotiation_request"}}) };
+
+        // Packet Error Counts
+        prometheus::simpleapi::counter_family_t packet_errors
+        { "zt_packet_incoming_error", "incoming packet errors"};
+        prometheus::simpleapi::counter_metric_t pkt_error_obj_not_found
+        { packet_errors.Add({{"error_type", "obj_not_found"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_error_unsupported_op
+        { packet_errors.Add({{"error_type", "unsupported_operation"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_error_identity_collision
+        { packet_errors.Add({{"error_type", "identity_collision"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_error_need_membership_cert
+        { packet_errors.Add({{"error_type", "need_membership_certificate"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_error_network_access_denied
+        { packet_errors.Add({{"error_type", "network_access_denied"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_error_unwanted_multicast
+        { packet_errors.Add({{"error_type", "unwanted_multicast"}}) };
+        prometheus::simpleapi::counter_metric_t pkt_error_authentication_required
+        { packet_errors.Add({{"error_type", "authentication_required"}}) };
+
         // Data Sent/Received Metrics
         prometheus::simpleapi::counter_metric_t udp_send
         { "zt_udp_data_sent", "number of bytes ZeroTier has sent via UDP" };
diff --git a/node/Metrics.hpp b/node/Metrics.hpp
index 9dd77017d..3b65ea4b9 100644
--- a/node/Metrics.hpp
+++ b/node/Metrics.hpp
@@ -22,6 +22,40 @@ namespace prometheus {
 
 namespace ZeroTier {
     namespace Metrics {
+        // Packet Type Counts
+        extern prometheus::simpleapi::counter_family_t packets;
+        extern prometheus::simpleapi::counter_metric_t pkt_error;
+        extern prometheus::simpleapi::counter_metric_t pkt_ack;
+        extern prometheus::simpleapi::counter_metric_t pkt_qos;
+        extern prometheus::simpleapi::counter_metric_t pkt_hello;
+        extern prometheus::simpleapi::counter_metric_t pkt_ok;
+        extern prometheus::simpleapi::counter_metric_t pkt_whois;
+        extern prometheus::simpleapi::counter_metric_t pkt_rendezvous;
+        extern prometheus::simpleapi::counter_metric_t pkt_frame;
+        extern prometheus::simpleapi::counter_metric_t pkt_ext_frame;
+        extern prometheus::simpleapi::counter_metric_t pkt_echo;
+        extern prometheus::simpleapi::counter_metric_t pkt_multicast_like;
+        extern prometheus::simpleapi::counter_metric_t pkt_network_credentials;
+        extern prometheus::simpleapi::counter_metric_t pkt_network_config_request;
+        extern prometheus::simpleapi::counter_metric_t pkt_network_config;
+        extern prometheus::simpleapi::counter_metric_t pkt_multicast_gather;
+        extern prometheus::simpleapi::counter_metric_t pkt_multicast_frame;
+        extern prometheus::simpleapi::counter_metric_t pkt_push_direct_paths;
+        extern prometheus::simpleapi::counter_metric_t pkt_user_message;
+        extern prometheus::simpleapi::counter_metric_t pkt_remote_trace;
+        extern prometheus::simpleapi::counter_metric_t pkt_path_negotiation_request;
+
+        // Packet Error Counts
+        extern prometheus::simpleapi::counter_family_t packet_errors;
+        extern prometheus::simpleapi::counter_metric_t pkt_error_obj_not_found;
+        extern prometheus::simpleapi::counter_metric_t pkt_error_unsupported_op;
+        extern prometheus::simpleapi::counter_metric_t pkt_error_identity_collision;
+        extern prometheus::simpleapi::counter_metric_t pkt_error_need_membership_cert;
+        extern prometheus::simpleapi::counter_metric_t pkt_error_network_access_denied;
+        extern prometheus::simpleapi::counter_metric_t pkt_error_unwanted_multicast;
+        extern prometheus::simpleapi::counter_metric_t pkt_error_authentication_required;
+
+
         // Data Sent/Received Metrics
         extern prometheus::simpleapi::counter_metric_t udp_send;
         extern prometheus::simpleapi::counter_metric_t udp_recv;