Repository: cilium/cilium
Pull request #12939 (2020-08-20 12:53:38)

monitor: Avoid JSON-encoding agent events for in-memory storage
This avoids encoding agent events in JSON before they are submitted to
the cilium monitor socket. This change will is in preparation for a
subsequent PR where Hubble will store and expose agents events in their
decoded form.

Signed-off-by: Sebastian Wicki <sebastian@isovalent.com>

Ref #12416

gandro: test-me-please

Patch:
diff --git a/.gitattributes b/.gitattributes
index 3050924faf..da25b05e45 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,4 @@
 go.sum linguist-generated
 examples/kubernetes/connectivity-check/connectivity-*.yaml linguist-generated
 examples/crds/*.yaml linguist-generated
+Documentation/cmdref/** linguist-generated
diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md
deleted file mode 100644
index 5394f7ed9a..0000000000
--- a/.github/ISSUE_TEMPLATE/issue_template.md
+++ /dev/null
@@ -1,18 +0,0 @@
----
-name: Support Request
-about: Support request or question related to Cilium
-title: ''
-labels: ''
-assignees: ''
-
----
-
-## Support
-
-<!--
-
-If you have usage questions, please try the [slack
-channel](http://cilium.io/slack) and see the [FAQ](https://goo.gl/qG2YmU)
-first.
-
--->
diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md
index ed364a4d03..f21e8032df 100644
--- a/Documentation/cmdref/cilium-agent.md
+++ b/Documentation/cmdref/cilium-agent.md
@@ -59,6 +59,7 @@ cilium-agent [flags]
       --disable-iptables-feeder-rules strings         Chains to ignore when installing feeder rules.
       --egress-masquerade-interfaces string           Limit egress masquerading to interface selector
       --enable-auto-protect-node-port-range           Append NodePort range to net.ipv4.ip_local_reserved_ports if it overlaps with ephemeral port range (net.ipv4.ip_local_port_range) (default true)
+      --enable-bandwidth-manager                      Enable BPF bandwidth manager
       --enable-bpf-clock-probe                        Enable BPF clock source probing for more efficient tick retrieval
       --enable-bpf-masquerade                         Masquerade packets from endpoints leaving the host with BPF instead of iptables
       --enable-endpoint-health-checking               Enable connectivity health checking between virtual endpoints (default true)
diff --git a/Documentation/cmdref/cilium_bpf.md b/Documentation/cmdref/cilium_bpf.md
index 38e4c247e1..94e8d45b2d 100644
--- a/Documentation/cmdref/cilium_bpf.md
+++ b/Documentation/cmdref/cilium_bpf.md
@@ -25,6 +25,7 @@ Direct access to local BPF maps
 ### SEE ALSO
 
 * [cilium](../cilium)	 - CLI
+* [cilium bpf bandwidth](../cilium_bpf_bandwidth)	 - BPF datapath bandwidth settings
 * [cilium bpf ct](../cilium_bpf_ct)	 - Connection tracking tables
 * [cilium bpf endpoint](../cilium_bpf_endpoint)	 - Local endpoint map
 * [cilium bpf fs](../cilium_bpf_fs)	 - BPF filesystem mount
diff --git a/Documentation/cmdref/cilium_bpf_bandwidth.md b/Documentation/cmdref/cilium_bpf_bandwidth.md
new file mode 100644
index 0000000000..ad1780d98f
--- /dev/null
+++ b/Documentation/cmdref/cilium_bpf_bandwidth.md
@@ -0,0 +1,29 @@
+<!-- This file was autogenerated via cilium cmdref, do not edit manually-->
+
+## cilium bpf bandwidth
+
+BPF datapath bandwidth settings
+
+### Synopsis
+
+BPF datapath bandwidth settings
+
+### Options
+
+```
+  -h, --help   help for bandwidth
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.cilium.yaml)
+  -D, --debug           Enable debug messages
+  -H, --host string     URI to server-side API
+```
+
+### SEE ALSO
+
+* [cilium bpf](../cilium_bpf)	 - Direct access to local BPF maps
+* [cilium bpf bandwidth list](../cilium_bpf_bandwidth_list)	 - List BPF datapath bandwidth settings
+
diff --git a/Documentation/cmdref/cilium_bpf_bandwidth_list.md b/Documentation/cmdref/cilium_bpf_bandwidth_list.md
new file mode 100644
index 0000000000..4c91646689
--- /dev/null
+++ b/Documentation/cmdref/cilium_bpf_bandwidth_list.md
@@ -0,0 +1,33 @@
+<!-- This file was autogenerated via cilium cmdref, do not edit manually-->
+
+## cilium bpf bandwidth list
+
+List BPF datapath bandwidth settings
+
+### Synopsis
+
+List BPF datapath bandwidth settings
+
+```
+cilium bpf bandwidth list [flags]
+```
+
+### Options
+
+```
+  -h, --help            help for list
+  -o, --output string   json| jsonpath='{}'
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.cilium.yaml)
+  -D, --debug           Enable debug messages
+  -H, --host string     URI to server-side API
+```
+
+### SEE ALSO
+
+* [cilium bpf bandwidth](../cilium_bpf_bandwidth)	 - BPF datapath bandwidth settings
+
diff --git a/Documentation/concepts/ebpf/maps.rst b/Documentation/concepts/ebpf/maps.rst
index 141bde8bbf..b7bac30afd 100644
--- a/Documentation/concepts/ebpf/maps.rst
+++ b/Documentation/concepts/ebpf/maps.rst
@@ -28,6 +28,8 @@ Proxy Map                node             512k            Max 512k concurrent re
 Tunnel                   node             64k             Max 32k nodes (IPv4+IPv6) or 64k nodes (IPv4 or IPv6) across all clusters
 IPv4 Fragmentation       node             8k              Max 8k fragmented datagrams in flight simultaneously on the node
 Session Affinity         node             64k             Max 64k affinities from different clients
+IP Masq                  node             16k             Max 16k IPv4 cidrs used by BPF-based ip-masq-agent
+Service Source Ranges    node             64k             Max 64k cumulative LB source ranges across all services
 ======================== ================ =============== =====================================================
 
 For some BPF maps, the upper capacity limit can be overridden using command
diff --git a/Documentation/configuration/metrics.rst b/Documentation/configuration/metrics.rst
index 7a1d22472e..9f4b59f73a 100644
--- a/Documentation/configuration/metrics.rst
+++ b/Documentation/configuration/metrics.rst
@@ -175,8 +175,10 @@ Endpoint
 ============================================ ================================================== ========================================================
 Name                                         Labels                                             Description
 ============================================ ================================================== ========================================================
-``endpoint_count``                                                                              Number of endpoints managed by this agent
-``endpoint_regenerations``                   ``outcome``                                        Count of all endpoint regenerations that have completed
+``endpoint``                                                                                    Number of endpoints managed by this agent
+``endpoint_count``                                                                              Number of endpoints managed by this agent (deprecated,  use ``endpoint``)
+``endpoint_regenerations``                   ``outcome``                                        Count of all endpoint regenerations that have completed (deprecated,  use ``endpoint_regenerations_total``)
+``endpoint_regenerations_total``             ``outcome``                                        Count of all endpoint regenerations that have completed
 ``endpoint_regeneration_time_stats_seconds`` ``scope``                                          Endpoint regeneration time stats
 ``endpoint_state``                           ``state``                                          Count of all endpoints
 ============================================ ================================================== ========================================================
@@ -206,14 +208,14 @@ Name                                          Labels
 eBPF
 ~~~~
 
-========================================== ================================================== ========================================================
-Name                                       Labels                                             Description
-========================================== ================================================== ========================================================
-``bpf_syscall_duration_seconds``           ``operation``, ``outcome``                         Duration of eBPF system call performed
-``bpf_map_ops_total``                      ``mapName``, ``operation``, ``outcome``            Number of eBPF map operations performed
-``bpf_maps_virtual_memory_max_bytes``                                                         Max memory used by eBPF maps installed in the system
-``bpf_progs_virtual_memory_max_bytes``                                                        Max memory used by eBPF programs installed in the system
-========================================== ================================================== ========================================================
+========================================== ===================================================================== ========================================================
+Name                                       Labels                                                                Description
+========================================== ===================================================================== ========================================================
+``bpf_syscall_duration_seconds``           ``operation``, ``outcome``                                            Duration of eBPF system call performed
+``bpf_map_ops_total``                      ``mapName`` (deprecated), ``map_name``, ``operation``, ``outcome``    Number of eBPF map operations performed. ``mapName`` is deprecated and will be removed in 1.10. Use ``map_name`` instead.
+``bpf_maps_virtual_memory_max_bytes``                                                                            Max memory used by eBPF maps installed in the system
+``bpf_progs_virtual_memory_max_bytes``                                                                           Max memory used by eBPF programs installed in the system
+========================================== ===================================================================== ========================================================
 
 Both ``bpf_maps_virtual_memory_max_bytes`` and ``bpf_progs_virtual_memory_max_bytes``
 are currently reporting the system-wide memory usage of eBPF that is directly
@@ -238,11 +240,13 @@ Policy
 ========================================== ================================================== ========================================================
 Name                                       Labels                                             Description
 ========================================== ================================================== ========================================================
-``policy_count``                                                                              Number of policies currently loaded
+``policy``                                                                                    Number of policies currently loaded
+``policy_count``                                                                              Number of policies currently loaded (deprecated, use ``policy``)
 ``policy_regeneration_total``                                                                 Total number of policies regenerated successfully
 ``policy_regeneration_time_stats_seconds`` ``scope``                                          Policy regeneration time stats labeled by the scope
 ``policy_max_revision``                                                                       Highest policy revision number in the agent
-``policy_import_errors``                                                                      Number of times a policy import has failed
+``policy_import_errors``                                                                      Number of times a policy import has failed (deprecated, use ``policy_import_errors_total``)
+``policy_import_errors_total``                                                                Number of times a policy import has failed
 ``policy_endpoint_enforcement_status``                                                        Number of endpoints labeled by policy enforcement status
 ========================================== ================================================== ========================================================
 
@@ -263,7 +267,8 @@ Identity
 ======================================== ================================================== ========================================================
 Name                                     Labels                                             Description
 ======================================== ================================================== ========================================================
-``identity_count``                                                                          Number of identities currently allocated
+``identity``                                                                                Number of identities currently allocated
+``identity_count``                                                                          Number of identities currently allocated (deprecated, use ``identity``)
 ======================================== ================================================== ========================================================
 
 Events external to Cilium
@@ -365,18 +370,18 @@ All metrics are exported under the ``cilium_operator_`` Prometheus namespace.
 IPAM
 ~~~~
 
-======================================== ================================ ========================================================
-Name                                     Labels                           Description
-======================================== ================================ ========================================================
-``ipam_ips``                             ``type``                         Number of IPs allocated
-``ipam_allocation_ops``                  ``subnetId``                     Number of IP allocation operations
-``ipam_interface_creation_ops``          ``subnetId``, ``status``         Number of interfaces creation operations
-``ipam_available``                                                        Number of interfaces with addresses available
-``ipam_nodes_at_capacity``                                                Number of nodes unable to allocate more addresses
-``ipam_resync_total``                                                     Number of synchronization operations with external IPAM API
-``ipam_api_duration_seconds``            ``operation``, ``responseCode``  Duration of interactions with external IPAM API
-``ipam_api_rate_limit_duration_seconds`` ``operation``                    Duration of rate limiting while accessing external IPAM API
-======================================== ================================ ========================================================
+======================================== ================================================================= ========================================================
+Name                                     Labels                                                            Description
+======================================== ================================================================= ========================================================
+``ipam_ips``                             ``type``                                                          Number of IPs allocated
+``ipam_allocation_ops``                  ``subnetId`` (deprecated), ``subnet_id``                          Number of IP allocation operations. ``subnetId`` is deprecated and will be removed in 1.10. Use ``subnet_id`` instead.
+``ipam_interface_creation_ops``          ``subnetId`` (deprecated), ``subnet_id``, ``status``              Number of interfaces creation operations. ``subnetId`` is deprecated and will be removed in 1.10. Use ``subnet_id`` instead.
+``ipam_available``                                                                                         Number of interfaces with addresses available
+``ipam_nodes_at_capacity``                                                                                 Number of nodes unable to allocate more addresses
+``ipam_resync_total``                                                                                      Number of synchronization operations with external IPAM API
+``ipam_api_duration_seconds``            ``operation``, ``responseCode`` (deprecated), ``response_code``   Duration of interactions with external IPAM API. ``responseCode`` is deprecated and will be removed in 1.10. Use ``response_code`` instead.
+``ipam_api_rate_limit_duration_seconds`` ``operation``                                                     Duration of rate limiting while accessing external IPAM API
+======================================== ================================================================= ========================================================
 
 Hubble
 ------
diff --git a/Documentation/gettingstarted/bandwidth-manager.rst b/Documentation/gettingstarted/bandwidth-manager.rst
new file mode 100644
index 0000000000..dcd7e5bf13
--- /dev/null
+++ b/Documentation/gettingstarted/bandwidth-manager.rst
@@ -0,0 +1,144 @@
+.. only:: not (epub or latex or html)
+
+    WARNING: You are looking at unreleased Cilium documentation.
+    Please use the official rendered version released here:
+    https://docs.cilium.io
+
+.. _bandwidth-manager:
+
+************************
+Bandwidth Manager (beta)
+************************
+
+This guide explains how to configure Cilium's bandwidth manager to
+optimize TCP and UDP workloads and efficiently rate limit individual Pods
+if needed through the help of EDT (Earliest Departure Time) and eBPF.
+
+The bandwidth manager does not rely on CNI chaining and is natively integrated
+into Cilium instead. Hence, it does not make use of the `bandwidth CNI
+<https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#support-traffic-shaping>`_
+plugin. Due to scalability concerns in particular for multi-queue network
+interfaces, it is not recommended to use the bandwidth CNI plugin which is
+based on TBF (Token Bucket Filter) instead of EDT.
+
+Cilium's bandwidth manager supports the ``kubernetes.io/egress-bandwidth`` Pod
+annotation which is enforced on egress at the native host networking devices.
+The bandwidth enforcement is supported for direct routing as well as tunneling
+mode in Cilium.
+
+The ``kubernetes.io/ingress-bandwidth`` annotation is not supported and also not
+recommended to use. Limiting bandwidth happens natively at the egress point of
+networking devices in order to reduce or pace bandwidth usage on the wire.
+Enforcing at ingress would add yet another layer of buffer queueing right in the
+critical fast-path of a node via ``ifb`` device where ingress traffic first needs
+to be redirected to the ``ifb``'s egress point in order to perform shaping before
+traffic can go up the stack. At this point traffic has already occupied the
+bandwidth usage on the wire, and the node has already spent resources on
+processing the packet. ``kubernetes.io/ingress-bandwidth`` annotation is ignored
+by Cilium's bandwidth manager.
+
+.. note::
+
+   Bandwidth Manager requires a v5.0.x or more recent Linux kernel.
+
+.. include:: k8s-install-download-release.rst
+
+The Cilium bandwidth manager is enabled by default for new deployments via Helm:
+
+.. parsed-literal::
+
+   helm install cilium |CHART_RELEASE| \\
+     --namespace kube-system
+
+The option for Helm is controllable through ``global.bandwidthManager`` with a
+possible setting of ``true`` (default) and ``false``.
+
+The native host networking devices are auto detected as native devices which have
+the default route on the host or have Kubernetes InternalIP or ExternalIP assigned.
+InternalIP is preferred over ExternalIP if both exist. To change and manually specify
+the devices, set their names in the ``global.devices`` helm option, e.g.
+``global.devices='{eth0,eth1,eth2}'``. Each listed device has to be named the same
+on all Cilium managed nodes.
+
+Verify that the Cilium Pod has come up correctly:
+
+.. code:: bash
+
+    kubectl -n kube-system get pods -l k8s-app=cilium
+    NAME                READY     STATUS    RESTARTS   AGE
+    cilium-crf7f        1/1       Running   0          10m
+
+In order to verify whether the bandwidth manager feature has been enabled in Cilium,
+the ``cilium status`` CLI command provides visibility through the ``BandwidthManager``
+info line. It also dumps a list of devices on which the egress bandwidth limitation
+is enforced:
+
+.. parsed-literal::
+
+    kubectl exec -it -n kube-system cilium-xxxxx -- cilium status | grep BandwidthManager
+    BandwidthManager:       EDT with BPF   [eth0]
+
+Assuming we have a multi-node cluster, in the next step, we deploy a netperf Pod on
+the node named foobar. The following example deployment yaml limits the egress
+bandwidth of the netperf Pod on the node's physical device:
+
+.. parsed-literal::
+
+    apiVersion: apps/v1
+    kind: Deployment
+    metadata:
+      name: netperf
+    spec:
+      selector:
+        matchLabels:
+          run: netperf
+      replicas: 1
+      template:
+        metadata:
+          labels:
+            run: netperf
+          annotations:
+            kubernetes.io/egress-bandwidth: "10M"
+        spec:
+          nodeName: foobar
+          containers:
+          - name: netperf
+            image: cilium/netperf
+            ports:
+            - containerPort: 12865
+
+Once up and running, ``netperf`` client can be invoked from a different node in the
+cluster using the Pod's IP (in this case ``10.217.0.254``) directly. The test streaming
+direction is from the netperf deployment towards the client, hence ``TCP_MAERTS``:
+
+.. parsed-literal::
+
+  netperf -t TCP_MAERTS -H 10.217.0.254
+  MIGRATED TCP MAERTS TEST from 0.0.0.0 (0.0.0.0) port 0 AF_INET to 10.217.0.254 () port 0 AF_INET
+  Recv   Send    Send
+  Socket Socket  Message  Elapsed
+  Size   Size    Size     Time     Throughput
+  bytes  bytes   bytes    secs.    10^6bits/sec
+
+   87380  16384  16384    10.00       9.56
+
+As can be seen, egress traffic of the netperf Pod has been limited to 10Mbit per second.
+
+In order to introspect current endpoint bandwidth settings from BPF side, the following
+command can be run:
+
+.. parsed-literal::
+
+    kubectl exec -it -n kube-system cilium-xxxxx -- cilium bpf bandwidth list
+    IDENTITY   EGRESS BANDWIDTH (BitsPerSec)
+    491        10M
+
+Each Pod is represented in Cilium as an :ref:`endpoint` which has an identity. The above
+identity can then be correlated with the ``cilium endpoint list`` command.
+
+Limitations
+###########
+
+    * Bandwidth enforcement currently does not work in combination with L7 Cilium Network Policies.
+      In case they select the Pod at egress, then the bandwidth enforcement will be disabled for
+      those Pods.
diff --git a/Documentation/gettingstarted/index.rst b/Documentation/gettingstarted/index.rst
index 1efe35f23e..ede8a1a8a4 100644
--- a/Documentation/gettingstarted/index.rst
+++ b/Documentation/gettingstarted/index.rst
@@ -63,6 +63,7 @@ Advanced Networking
    encryption
    host-services
    kubeproxy-free
+   bandwidth-manager
    kata
    ipam
 
diff --git a/Documentation/gettingstarted/kubeproxy-free.rst b/Documentation/gettingstarted/kubeproxy-free.rst
index fb5086d6d5..d4d06d155f 100644
--- a/Documentation/gettingstarted/kubeproxy-free.rst
+++ b/Documentation/gettingstarted/kubeproxy-free.rst
@@ -987,6 +987,27 @@ a fixed cookie value as a trade-off. This makes all applications on the host to
 select the same service endpoint for a given service with session affinity configured.
 To disable the feature, set ``config.sessionAffinity=false``.
 
+LoadBalancer Source Ranges Checks
+*********************************
+
+When a ``LoadBalancer`` service is configured with ``spec.loadBalancerSourceRanges``,
+Cilium's eBPF kube-proxy replacement restricts access from outside (e.g. external
+world traffic) to the service to the white-listed CIDRs specified in the field. If
+the field is empty, no restrictions for the access will be applied.
+
+When accessing the service from inside a cluster, the kube-proxy replacement will
+ignore the field regardless whether it is set. This means that any pod or any host
+process in the cluster will be able to access the ``LoadBalancer`` service internally.
+
+The load balancer source range check feature is enabled by default, and it can be
+disabled by setting ``config.svcSourceRangeCheck=false``. It makes sense to disable
+the check when running on some cloud providers. E.g. `Amazon NLB
+<https://kubernetes.io/docs/concepts/services-networking/service/#aws-nlb-support>`__
+natively implements the check, so the kube-proxy replacement's feature can be disabled.
+Meanwhile `GKE internal TCP/UDP load balancer
+<https://cloud.google.com/kubernetes-engine/docs/how-to/internal-load-balancing#lb_source_ranges>`__
+does not, so the feature must be kept enabled in order to restrict the access.
+
 Limitations
 ###########
 
diff --git a/Documentation/install/upgrade.rst b/Documentation/install/upgrade.rst
index 8312866f32..b19736ca06 100644
--- a/Documentation/install/upgrade.rst
+++ b/Documentation/install/upgrade.rst
@@ -336,9 +336,31 @@ The following metrics have been renamed:
 New Metrics
 ~~~~~~~~~~~
 
+  * ``cilium_endpoint_regenerations_total`` counts of all endpoint regenerations that have completed, tagged by outcome.
+  * ``cilium_k8s_client_api_calls_total`` is number of API calls made to kube-apiserver labeled by host, method and return code.
   * ``cilium_kvstore_quorum_errors_total`` counts the number of kvstore quorum
     loss errors. The label ``error`` indicates the type of error.
 
+Deprecated Metrics/Labels
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+  * ``cilium_endpoint_regenerations`` is deprecated and will be removed in 1.10. Please use ``cilium_endpoint_regenerations_total`` instead.
+  * ``cilium_k8s_client_api_calls_counter``is deprecated and will be removed in 1.10. Please use ``cilium_k8s_client_api_calls_total`` instead.
+  * ``cilium_identity_count`` is deprecated and will be removed in 1.10. Please use ``cilium_identity`` instead.
+  * ``cilium_policy_count`` is deprecated and will be removed in 1.10. Please use ``cilium_policy`` instead.
+  * ``cilium_policy_import_errors`` is deprecated and will be removed in 1.10. Please use ``cilium_policy_import_errors_total`` instead.
+  * Label ``mapName`` in ``cilium_bpf_map_ops_total`` is deprecated and will be removed in 1.10. Please use label ``map_name`` instead.
+  * Label ``eventType`` in ``cilium_nodes_all_events_received_total`` is deprecated and will be removed in 1.10. Please use
+    label ``event_type`` instead.
+  * Label ``responseCode`` in ``*api_duration_seconds`` is deprecated and will be removed in 1.10. Please use
+    label ``response_code`` instead.
+  * Label ``subnetId`` in ``cilium_operator_ipam_allocation_ops`` is deprecated and will be removed in 1.10. Please use
+    label ``subnet_id`` instead.
+  * Label ``subnetId`` in ``cilium_operator_ipam_release_ops`` is deprecated and will be removed in 1.10. Please use
+    label ``subnet_id`` instead.
+  * Label ``subnetId`` and ``availabilityZone`` in ``cilium_operator_ipam_available_ips_per_subnet`` are deprecated and will be removed in 1.10. Please use
+    label ``subnet_id`` and ``availability_zone`` instead.
+
 Removed options
 ~~~~~~~~~~~~~~~
 
diff --git a/Documentation/spelling_wordlist.txt b/Documentation/spelling_wordlist.txt
index 3b7d689abd..b3c2cb2b82 100644
--- a/Documentation/spelling_wordlist.txt
+++ b/Documentation/spelling_wordlist.txt
@@ -92,6 +92,7 @@ Lund
 Lx
 Majkowski
 Marek
+Mbit
 Meetup
 Mellanox
 Memcached
@@ -237,6 +238,7 @@ cassandra
 cassandraparser
 cgroup
 ci
+cidrs
 classful
 classid
 cli
@@ -503,6 +505,7 @@ netdevice
 netdevices
 netdevsim
 netfilter
+netperf
 netsec
 netvsc
 newproto
diff --git a/api/v1/models/bandwidth_manager.go b/api/v1/models/bandwidth_manager.go
new file mode 100644
index 0000000000..9e97e198a9
--- /dev/null
+++ b/api/v1/models/bandwidth_manager.go
@@ -0,0 +1,51 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+// Copyright 2017-2020 Authors of Cilium
+// SPDX-License-Identifier: Apache-2.0
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"github.com/go-openapi/strfmt"
+	"github.com/go-openapi/swag"
+)
+
+// BandwidthManager Status of bandwidth manager
+//
+// +k8s:deepcopy-gen=true
+//
+// swagger:model BandwidthManager
+type BandwidthManager struct {
+
+	// devices
+	Devices []string `json:"devices"`
+
+	// Is bandwidth manager enabled
+	Enabled bool `json:"enabled,omitempty"`
+}
+
+// Validate validates this bandwidth manager
+func (m *BandwidthManager) Validate(formats strfmt.Registry) error {
+	return nil
+}
+
+// MarshalBinary interface implementation
+func (m *BandwidthManager) MarshalBinary() ([]byte, error) {
+	if m == nil {
+		return nil, nil
+	}
+	return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *BandwidthManager) UnmarshalBinary(b []byte) error {
+	var res BandwidthManager
+	if err := swag.ReadJSON(b, &res); err != nil {
+		return err
+	}
+	*m = res
+	return nil
+}
diff --git a/api/v1/models/status_response.go b/api/v1/models/status_response.go
index 3427f45523..6688e2379f 100644
--- a/api/v1/models/status_response.go
+++ b/api/v1/models/status_response.go
@@ -22,6 +22,9 @@ import (
 // swagger:model StatusResponse
 type StatusResponse struct {
 
+	// Status of bandwidth manager
+	BandwidthManager *BandwidthManager `json:"bandwidth-manager,omitempty"`
+
 	// Status of BPF maps
 	BpfMaps *BPFMapStatus `json:"bpf-maps,omitempty"`
 
@@ -78,6 +81,10 @@ type StatusResponse struct {
 func (m *StatusResponse) Validate(formats strfmt.Registry) error {
 	var res []error
 
+	if err := m.validateBandwidthManager(formats); err != nil {
+		res = append(res, err)
+	}
+
 	if err := m.validateBpfMaps(formats); err != nil {
 		res = append(res, err)
 	}
@@ -144,6 +151,24 @@ func (m *StatusResponse) Validate(formats strfmt.Registry) error {
 	return nil
 }
 
+func (m *StatusResponse) validateBandwidthManager(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.BandwidthManager) { // not required
+		return nil
+	}
+
+	if m.BandwidthManager != nil {
+		if err := m.BandwidthManager.Validate(formats); err != nil {
+			if ve, ok := err.(*errors.Validation); ok {
+				return ve.ValidateName("bandwidth-manager")
+			}
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (m *StatusResponse) validateBpfMaps(formats strfmt.Registry) error {
 
 	if swag.IsZero(m.BpfMaps) { // not required
diff --git a/api/v1/models/zz_generated.deepcopy.go b/api/v1/models/zz_generated.deepcopy.go
index 08ba3e693f..12a8edcc90 100644
--- a/api/v1/models/zz_generated.deepcopy.go
+++ b/api/v1/models/zz_generated.deepcopy.go
@@ -49,6 +49,27 @@ func (in *BPFMapStatus) DeepCopy() *BPFMapStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BandwidthManager) DeepCopyInto(out *BandwidthManager) {
+	*out = *in
+	if in.Devices != nil {
+		in, out := &in.Devices, &out.Devices
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BandwidthManager.
+func (in *BandwidthManager) DeepCopy() *BandwidthManager {
+	if in == nil {
+		return nil
+	}
+	out := new(BandwidthManager)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CIDRPolicy) DeepCopyInto(out *CIDRPolicy) {
 	*out = *in
@@ -732,6 +753,11 @@ func (in *RequestResponseStatistics) DeepCopy() *RequestResponseStatistics {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StatusResponse) DeepCopyInto(out *StatusResponse) {
 	*out = *in
+	if in.BandwidthManager != nil {
+		in, out := &in.BandwidthManager, &out.BandwidthManager
+		*out = new(BandwidthManager)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.BpfMaps != nil {
 		in, out := &in.BpfMaps, &out.BpfMaps
 		*out = new(BPFMapStatus)
diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml
index c32a1e2a27..92fbaa8dca 100644
--- a/api/v1/openapi.yaml
+++ b/api/v1/openapi.yaml
@@ -1597,6 +1597,9 @@ definitions:
       proxy:
         description: Status of proxy
         "$ref": "#/definitions/ProxyStatus"
+      bandwidth-manager:
+        description: Status of bandwidth manager
+        "$ref": "#/definitions/BandwidthManager"
       masquerading:
         description: Status of masquerading
         "$ref": "#/definitions/Masquerading"
@@ -2380,6 +2383,20 @@ definitions:
       forwarded:
         description: Number of messages forwarded
         type: integer
+  BandwidthManager:
+    description: |-
+      Status of bandwidth manager
+
+      +k8s:deepcopy-gen=true
+    type: object
+    properties:
+      enabled:
+        description: Is bandwidth manager enabled
+        type: boolean
+      devices:
+        type: array
+        items:
+          type: string
   Masquerading:
     description: |-
       Status of masquerading
diff --git a/api/v1/server/embedded_spec.go b/api/v1/server/embedded_spec.go
index e52d56536d..a3ec578731 100644
--- a/api/v1/server/embedded_spec.go
+++ b/api/v1/server/embedded_spec.go
@@ -1432,6 +1432,22 @@ func init() {
         }
       }
     },
+    "BandwidthManager": {
+      "description": "Status of bandwidth manager\n\n+k8s:deepcopy-gen=true",
+      "type": "object",
+      "properties": {
+        "devices": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "enabled": {
+          "description": "Is bandwidth manager enabled",
+          "type": "boolean"
+        }
+      }
+    },
     "CIDRList": {
       "description": "List of CIDRs",
       "type": "object",
@@ -3286,6 +3302,10 @@ func init() {
       "description": "Health and status information of daemon\n\n+k8s:deepcopy-gen=true",
       "type": "object",
       "properties": {
+        "bandwidth-manager": {
+          "description": "Status of bandwidth manager",
+          "$ref": "#/definitions/BandwidthManager"
+        },
         "bpf-maps": {
           "description": "Status of BPF maps",
           "$ref": "#/definitions/BPFMapStatus"
@@ -5105,6 +5125,22 @@ func init() {
         }
       }
     },
+    "BandwidthManager": {
+      "description": "Status of bandwidth manager\n\n+k8s:deepcopy-gen=true",
+      "type": "object",
+      "properties": {
+        "devices": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "enabled": {
+          "description": "Is bandwidth manager enabled",
+          "type": "boolean"
+        }
+      }
+    },
     "CIDRList": {
       "description": "List of CIDRs",
       "type": "object",
@@ -7240,6 +7276,10 @@ func init() {
       "description": "Health and status information of daemon\n\n+k8s:deepcopy-gen=true",
       "type": "object",
       "properties": {
+        "bandwidth-manager": {
+          "description": "Status of bandwidth manager",
+          "$ref": "#/definitions/BandwidthManager"
+        },
         "bpf-maps": {
           "description": "Status of BPF maps",
           "$ref": "#/definitions/BPFMapStatus"
diff --git a/bpf/Makefile b/bpf/Makefile
index a0c355d764..d3ca769e43 100644
--- a/bpf/Makefile
+++ b/bpf/Makefile
@@ -56,6 +56,8 @@ LB_OPTIONS = \
 	-DENABLE_IPV4:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY \
 	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY \
 	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_SRC_RANGE_CHECK \
+	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_BANDWIDTH_MANAGER \
+	-DENABLE_IPV6:-DENABLE_IPSEC:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_NODEPORT_ACCELERATION:-DENABLE_SESSION_AFFINITY:-DENABLE_BANDWIDTH_MANAGER:-DENABLE_SRC_RANGE_CHECK
 
 # These options are intended to max out the BPF program complexity. it is load
 # tested as well.
diff --git a/bpf/bpf_alignchecker.c b/bpf/bpf_alignchecker.c
index dcbcb89dbe..95c70c81d9 100644
--- a/bpf/bpf_alignchecker.c
+++ b/bpf/bpf_alignchecker.c
@@ -81,6 +81,8 @@ int main(void)
 	DECLARE(struct, lb_affinity_match, iter);
 	DECLARE(struct, lb4_src_range_key, iter);
 	DECLARE(struct, lb6_src_range_key, iter);
+	DECLARE(struct, edt_id, iter);
+	DECLARE(struct, edt_info, iter);
 
 	return 0;
 }
diff --git a/bpf/bpf_host.c b/bpf/bpf_host.c
index aa30cbe003..dab48a5455 100644
--- a/bpf/bpf_host.c
+++ b/bpf/bpf_host.c
@@ -26,6 +26,7 @@
 #endif
 
 #include "lib/common.h"
+#include "lib/edt.h"
 #include "lib/arp.h"
 #include "lib/maps.h"
 #include "lib/ipv6.h"
@@ -883,6 +884,10 @@ int from_netdev(struct __ctx_buff *ctx)
 __section("from-host")
 int from_host(struct __ctx_buff *ctx)
 {
+	/* Traffic from the host ns going through cilium_host device must
+	 * not be subject to EDT rate-limiting.
+	 */
+	edt_set_aggregate(ctx, 0);
 	return handle_netdev(ctx, true);
 }
 
@@ -927,14 +932,22 @@ int to_netdev(struct __ctx_buff *ctx __maybe_unused)
 		ret = DROP_UNKNOWN_L3;
 		break;
 	}
-
-
 out:
 	if (IS_ERR(ret))
 		return send_drop_notify_error(ctx, src_id, ret, CTX_ACT_DROP,
 					      METRIC_EGRESS);
 #endif /* ENABLE_HOST_FIREWALL */
 
+#if defined(ENABLE_BANDWIDTH_MANAGER)
+	ret = edt_sched_departure(ctx);
+	/* No send_drop_notify_error() here given we're rate-limiting. */
+	if (ret == CTX_ACT_DROP) {
+		update_metrics(ctx_full_len(ctx), METRIC_EGRESS,
+			       -DROP_EDT_HORIZON);
+		return ret;
+	}
+#endif
+
 #if defined(ENABLE_NODEPORT) && \
 	(!defined(ENABLE_DSR) || \
 	 (defined(ENABLE_DSR) && defined(ENABLE_DSR_HYBRID)) || \
diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index 332d51e224..1f044f9e86 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -16,6 +16,7 @@
 #include "lib/config.h"
 #include "lib/maps.h"
 #include "lib/arp.h"
+#include "lib/edt.h"
 #include "lib/ipv6.h"
 #include "lib/ipv4.h"
 #include "lib/icmp6.h"
@@ -801,6 +802,7 @@ int handle_xgress(struct __ctx_buff *ctx)
 	int ret;
 
 	bpf_clear_meta(ctx);
+	edt_set_aggregate(ctx, LXC_ID);
 
 	send_trace_notify(ctx, TRACE_FROM_LXC, SECLABEL, 0, 0, 0, 0,
 			  TRACE_PAYLOAD_LEN);
diff --git a/bpf/include/bpf/ctx/skb.h b/bpf/include/bpf/ctx/skb.h
index c821347d45..204b15295a 100644
--- a/bpf/include/bpf/ctx/skb.h
+++ b/bpf/include/bpf/ctx/skb.h
@@ -68,6 +68,12 @@ ctx_full_len(const struct __sk_buff *ctx)
 	return ctx->len;
 }
 
+static __always_inline __maybe_unused __u32
+ctx_wire_len(const struct __sk_buff *ctx)
+{
+	return ctx->wire_len;
+}
+
 static __always_inline __maybe_unused void
 ctx_store_meta(struct __sk_buff *ctx, const __u32 off, __u32 data)
 {
diff --git a/bpf/include/bpf/ctx/xdp.h b/bpf/include/bpf/ctx/xdp.h
index 48cd35d0d6..df8c300db4 100644
--- a/bpf/include/bpf/ctx/xdp.h
+++ b/bpf/include/bpf/ctx/xdp.h
@@ -251,6 +251,12 @@ ctx_full_len(const struct xdp_md *ctx)
 	return ctx_data_end(ctx) - ctx_data(ctx);
 }
 
+static __always_inline __maybe_unused __u32
+ctx_wire_len(const struct xdp_md *ctx)
+{
+	return ctx_full_len(ctx);
+}
+
 struct bpf_elf_map __section_maps cilium_xdp_scratch = {
 	.type		= BPF_MAP_TYPE_PERCPU_ARRAY,
 	.size_key	= sizeof(int),
diff --git a/bpf/include/linux/bpf.h b/bpf/include/linux/bpf.h
index f13e3e6f21..bc5e109b57 100644
--- a/bpf/include/linux/bpf.h
+++ b/bpf/include/linux/bpf.h
@@ -3590,19 +3590,12 @@ struct __sk_buff {
 	/* ... here. */
 
 	__u32 data_meta;
-
-#if 0
-	/* TODO: compiled out for now since this cases verifier breakage
-	 * on older kernels resulting in invalid bpf_context access. Needs
-	 * follow-up investigation.
-	 */
 	__bpf_md_ptr(struct bpf_flow_keys *, flow_keys);
 	__u64 tstamp;
 	__u32 wire_len;
 	__u32 gso_segs;
 	__bpf_md_ptr(struct bpf_sock *, sk);
 	__u32 gso_size;
-#endif
 };
 
 struct bpf_tunnel_key {
@@ -3927,7 +3920,12 @@ struct bpf_sock_addr {
 	__u32 msg_src_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.
 				 * Stored in network byte order.
 				 */
-#if 0 /* TODO, see comment in __sk_buff */
+
+	/* TODO: compiled out for now since this cases verifier breakage
+	 * on older kernels resulting in invalid bpf_context access. Needs
+	 * follow-up investigation.
+	 */
+#if 0
 	__bpf_md_ptr(struct bpf_sock *, sk);
 #endif
 };
diff --git a/bpf/lib/common.h b/bpf/lib/common.h
index fbbdc190bf..7e741b745b 100644
--- a/bpf/lib/common.h
+++ b/bpf/lib/common.h
@@ -208,6 +208,17 @@ struct endpoint_info {
 	__u32		pad[4];
 };
 
+struct edt_id {
+	__u64		id;
+};
+
+struct edt_info {
+	__u64		bps;
+	__u64		t_last;
+	__u64		t_horizon_drop;
+	__u64		pad[4];
+};
+
 struct remote_endpoint_info {
 	__u32		sec_label;
 	__u32		tunnel_endpoint;
@@ -347,11 +358,11 @@ enum {
 #define DROP_UNUSED8		-159 /* unused */
 #define DROP_NO_TUNNEL_ENDPOINT -160
 #define DROP_UNUSED9		-161 /* unused */
-#define DROP_UNUSED10		-162 /* unused */
-#define DROP_UNKNOWN_CT			-163
-#define DROP_HOST_UNREACHABLE		-164
+#define DROP_EDT_HORIZON	-162
+#define DROP_UNKNOWN_CT		-163
+#define DROP_HOST_UNREACHABLE	-164
 #define DROP_NO_CONFIG		-165
-#define DROP_UNSUPPORTED_L2		-166
+#define DROP_UNSUPPORTED_L2	-166
 #define DROP_NAT_NO_MAPPING	-167
 #define DROP_NAT_UNSUPP_PROTO	-168
 #define DROP_NO_FIB		-169
diff --git a/bpf/lib/edt.h b/bpf/lib/edt.h
new file mode 100644
index 0000000000..c00ebeda0b
--- /dev/null
+++ b/bpf/lib/edt.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2020 Authors of Cilium */
+
+#ifndef __EDT_H_
+#define __EDT_H_
+
+#include "common.h"
+#include "time.h"
+#include "maps.h"
+
+#ifdef ENABLE_BANDWIDTH_MANAGER
+static __always_inline void edt_set_aggregate(struct __ctx_buff *ctx,
+					      __u32 aggregate)
+{
+	/* 16 bit as current used aggregate, and preserved in host ns. */
+	ctx->queue_mapping = aggregate;
+}
+
+static __always_inline __u32 edt_get_aggregate(struct __ctx_buff *ctx)
+{
+	__u32 aggregate = ctx->queue_mapping;
+
+	/* We need to reset queue mapping here such that new mapping will
+	 * be performed based on skb hash. See netdev_pick_tx().
+	 */
+	ctx->queue_mapping = 0;
+
+	return aggregate;
+}
+
+static __always_inline int edt_sched_departure(struct __ctx_buff *ctx)
+{
+	__u64 delay, now, t, t_next;
+	struct edt_id aggregate;
+	struct edt_info *info;
+	__u16 proto;
+
+	if (!validate_ethertype(ctx, &proto))
+		return CTX_ACT_OK;
+	if (proto != bpf_htons(ETH_P_IP) &&
+	    proto != bpf_htons(ETH_P_IPV6))
+		return CTX_ACT_OK;
+
+	aggregate.id = edt_get_aggregate(ctx);
+	if (!aggregate.id)
+		return CTX_ACT_OK;
+
+	info = map_lookup_elem(&THROTTLE_MAP, &aggregate);
+	if (!info)
+		return CTX_ACT_OK;
+
+	now = ktime_get_ns();
+	t = ctx->tstamp;
+	if (t < now)
+		t = now;
+	delay = ((__u64)ctx_wire_len(ctx)) * NSEC_PER_SEC / info->bps;
+	t_next = READ_ONCE(info->t_last) + delay;
+	if (t_next <= t) {
+		WRITE_ONCE(info->t_last, t);
+		return CTX_ACT_OK;
+	}
+	/* FQ implements a drop horizon, see also 39d010504e6b ("net_sched:
+	 * sch_fq: add horizon attribute"). However, we explicitly need the
+	 * drop horizon here to i) avoid having t_last messed up and ii) to
+	 * potentially allow for per aggregate control.
+	 */
+	if (t_next - now >= info->t_horizon_drop)
+		return CTX_ACT_DROP;
+	WRITE_ONCE(info->t_last, t_next);
+	ctx->tstamp = t_next;
+	return CTX_ACT_OK;
+}
+#else
+static __always_inline void
+edt_set_aggregate(struct __ctx_buff *ctx __maybe_unused,
+		  __u32 aggregate __maybe_unused)
+{
+}
+#endif /* ENABLE_BANDWIDTH_MANAGER */
+#endif /* __EDT_H_ */
diff --git a/bpf/lib/maps.h b/bpf/lib/maps.h
index 9cbb9ddadd..bfc21f442f 100644
--- a/bpf/lib/maps.h
+++ b/bpf/lib/maps.h
@@ -42,6 +42,17 @@ struct bpf_elf_map __section_maps POLICY_CALL_MAP = {
 };
 #endif /* SKIP_POLICY_MAP */
 
+#ifdef ENABLE_BANDWIDTH_MANAGER
+struct bpf_elf_map __section_maps THROTTLE_MAP = {
+	.type		= BPF_MAP_TYPE_HASH,
+	.size_key	= sizeof(struct edt_id),
+	.size_value	= sizeof(struct edt_info),
+	.pinning	= PIN_GLOBAL_NS,
+	.max_elem	= THROTTLE_MAP_SIZE,
+	.flags		= BPF_F_NO_PREALLOC,
+};
+#endif /* ENABLE_BANDWIDTH_MANAGER */
+
 /* Map to link endpoint id to per endpoint cilium_policy map */
 #ifdef SOCKMAP
 struct bpf_elf_map __section_maps EP_POLICY_MAP = {
diff --git a/bpf/lib/overloadable_skb.h b/bpf/lib/overloadable_skb.h
index 7a563f872d..7b66a722e1 100644
--- a/bpf/lib/overloadable_skb.h
+++ b/bpf/lib/overloadable_skb.h
@@ -9,11 +9,11 @@ bpf_clear_meta(struct __sk_buff *ctx)
 {
 	__u32 zero = 0;
 
-	ctx->cb[0] = zero;
-	ctx->cb[1] = zero;
-	ctx->cb[2] = zero;
-	ctx->cb[3] = zero;
-	ctx->cb[4] = zero;
+	WRITE_ONCE(ctx->cb[0], zero);
+	WRITE_ONCE(ctx->cb[1], zero);
+	WRITE_ONCE(ctx->cb[2], zero);
+	WRITE_ONCE(ctx->cb[3], zero);
+	WRITE_ONCE(ctx->cb[4], zero);
 }
 
 /**
diff --git a/bpf/node_config.h b/bpf/node_config.h
index 7d0598ef12..d1cac7e9c9 100644
--- a/bpf/node_config.h
+++ b/bpf/node_config.h
@@ -91,6 +91,8 @@ DEFINE_IPV6(HOST_IP, 0xbe, 0xef, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0xa, 0x
 #define LB4_AFFINITY_MAP test_cilium_lb4_affinity
 #define LB6_AFFINITY_MAP test_cilium_lb6_affinity
 #define LB_AFFINITY_MATCH_MAP test_cilium_lb_affinity_match
+#define THROTTLE_MAP test_cilium_throttle
+#define THROTTLE_MAP_SIZE 65536
 #define ENABLE_ARP_RESPONDER
 #define TUNNEL_ENDPOINT_MAP_SIZE 65536
 #define ENDPOINTS_MAP_SIZE 65536
diff --git a/bugtool/cmd/configuration.go b/bugtool/cmd/configuration.go
index 8939cdf0b1..f37590c45a 100644
--- a/bugtool/cmd/configuration.go
+++ b/bugtool/cmd/configuration.go
@@ -54,10 +54,13 @@ func defaultCommands(confDir string, cmdDir string, k8sPods []string) []string {
 		"ip -6 n",
 		"ss -t -p -a -i -s",
 		"ss -u -p -a -i -s",
+		"tc qdisc show",
+		"tc -d -s qdisc show",
 		"uname -a",
 		"top -b -n 1",
 		"uptime",
 		"dmesg --time-format=iso",
+		"sysctl -a",
 		"bpftool map show",
 		"bpftool prog show",
 		// LB and CT map for debugging services; using bpftool for a reliable dump
@@ -231,6 +234,7 @@ func copyCiliumInfoCommands(cmdDir string, k8sPods []string) []string {
 		"cilium metrics list",
 		"cilium fqdn cache list",
 		"cilium config",
+		"cilium bpf bandwidth list",
 		"cilium bpf tunnel list",
 		"cilium bpf lb list",
 		"cilium bpf endpoint list",
diff --git a/cilium/cmd/bpf_bandwidth.go b/cilium/cmd/bpf_bandwidth.go
new file mode 100644
index 0000000000..d374e8f8ad
--- /dev/null
+++ b/cilium/cmd/bpf_bandwidth.go
@@ -0,0 +1,29 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+// bpfBandwidthCmd represents the bpf_bandwidth command
+var bpfBandwidthCmd = &cobra.Command{
+	Use:   "bandwidth",
+	Short: "BPF datapath bandwidth settings",
+}
+
+func init() {
+	bpfCmd.AddCommand(bpfBandwidthCmd)
+}
diff --git a/cilium/cmd/bpf_bandwidth_list.go b/cilium/cmd/bpf_bandwidth_list.go
new file mode 100644
index 0000000000..80d4656b80
--- /dev/null
+++ b/cilium/cmd/bpf_bandwidth_list.go
@@ -0,0 +1,104 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"fmt"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+
+	"github.com/spf13/cobra"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+var bpfBandwidthListCmd = &cobra.Command{
+	Use:   "list",
+	Short: "List BPF datapath bandwidth settings",
+	Run: func(cmd *cobra.Command, args []string) {
+		common.RequireRootPrivilege("cilium bpf bandwidth list")
+
+		bpfBandwidthList := make(map[string][]string)
+		if err := bwmap.ThrottleMap.Dump(bpfBandwidthList); err != nil {
+			fmt.Fprintf(os.Stderr, "error dumping contents of map: %s\n", err)
+			os.Exit(1)
+		}
+
+		if command.OutputJSON() {
+			if err := command.PrintOutput(bpfBandwidthList); err != nil {
+				fmt.Fprintf(os.Stderr, "error getting output of map in JSON: %s\n", err)
+				os.Exit(1)
+			}
+			return
+		}
+
+		listBandwidth(bpfBandwidthList)
+	},
+}
+
+func listBandwidth(bpfBandwidthList map[string][]string) {
+	if len(bpfBandwidthList) == 0 {
+		fmt.Fprintf(os.Stderr, "No entries found.\n")
+		return
+	}
+
+	const (
+		labelsIDTitle   = "IDENTITY"
+		labelsBandwidth = "EGRESS BANDWIDTH (BitsPerSec)"
+	)
+
+	w := tabwriter.NewWriter(os.Stdout, 5, 0, 3, ' ', 0)
+	fmt.Fprintf(w, "%s\t%s\n", labelsIDTitle, labelsBandwidth)
+
+	const numColumns = 2
+	rows := [][numColumns]string{}
+
+	for key, value := range bpfBandwidthList {
+		bps, _ := strconv.Atoi(value[0])
+		bps *= 8
+		quantity := resource.NewQuantity(int64(bps), resource.DecimalSI)
+		rows = append(rows, [numColumns]string{key, quantity.String()})
+	}
+
+	sort.Slice(rows, func(i, j int) bool {
+		for k := 0; k < numColumns; k++ {
+			c := strings.Compare(rows[i][k], rows[j][k])
+
+			if c != 0 {
+				return c < 0
+			}
+		}
+
+		return false
+	})
+
+	for _, r := range rows {
+		fmt.Fprintf(w, "%s\t%s\n", r[0], r[1])
+	}
+
+	w.Flush()
+}
+
+func init() {
+	bpfBandwidthCmd.AddCommand(bpfBandwidthListCmd)
+	command.AddJSONOutput(bpfBandwidthListCmd)
+}
diff --git a/contrib/coccinelle/const.cocci b/contrib/coccinelle/const.cocci
index 5d1e6515ef..53a6e66fdf 100644
--- a/contrib/coccinelle/const.cocci
+++ b/contrib/coccinelle/const.cocci
@@ -43,6 +43,7 @@ position p;
       when != &x->z
       when != e = x
       when != WRITE_ONCE(x->z, ...)
+      when != WRITE_ONCE(x->z[...], ...)
       when != f(..., x, ...)
       // Special case for the addr field since it's an array in several
       // structures.
diff --git a/daemon/cmd/daemon.go b/daemon/cmd/daemon.go
index f1d85c2171..99e10e65bb 100644
--- a/daemon/cmd/daemon.go
+++ b/daemon/cmd/daemon.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/cilium/cilium/api/v1/models"
 	health "github.com/cilium/cilium/cilium-health/launch"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/clustermesh"
 	"github.com/cilium/cilium/pkg/controller"
@@ -201,6 +202,8 @@ func (d *Daemon) init() error {
 	sockops.SkmsgDisable()
 
 	if !option.Config.DryMode {
+		bandwidth.InitBandwidthManager()
+
 		if err := d.createNodeConfigHeaderfile(); err != nil {
 			return err
 		}
@@ -295,11 +298,13 @@ func NewDaemon(ctx context.Context, epMgr *endpointmanager.EndpointManager, dp d
 	}
 
 	identity.IterateReservedIdentities(func(_ string, _ identity.NumericIdentity) {
+		metrics.Identity.Inc()
 		metrics.IdentityCount.Inc()
 	})
 	if option.Config.EnableWellKnownIdentities {
 		// Must be done before calling policy.NewPolicyRepository() below.
 		num := identity.InitWellKnownIdentities(option.Config)
+		metrics.Identity.Add(float64(num))
 		metrics.IdentityCount.Add(float64(num))
 	}
 
@@ -432,12 +437,17 @@ func NewDaemon(ctx context.Context, epMgr *endpointmanager.EndpointManager, dp d
 		bootstrapStats.k8sInit.End(true)
 	}
 
+	// Perform an early probe on the underlying kernel on whether BandwidthManager
+	// can be supported or not. This needs to be done before detectNativeDevices()
+	// as BandwidthManager needs these to be available for setup.
+	bandwidth.ProbeBandwidthManager()
+
 	// The kube-proxy replacement and host-fw devices detection should happen after
 	// establishing a connection to kube-apiserver, but before starting a k8s watcher.
 	// This is because the device detection requires self (Cilium)Node object,
 	// and the k8s service watcher depends on option.Config.EnableNodePort flag
 	// which can be modified after the device detection.
-	detectDevicesForNodePortAndHostFirewall(isKubeProxyReplacementStrict)
+	detectNativeDevices(isKubeProxyReplacementStrict)
 	finishKubeProxyReplacementInit(isKubeProxyReplacementStrict)
 
 	// BPF masquerade depends on BPF NodePort, so the following checks should
@@ -727,12 +737,11 @@ func numWorkerThreads() int {
 }
 
 // SendNotification sends an agent notification to the monitor
-func (d *Daemon) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (d *Daemon) SendNotification(notification monitorAPI.AgentNotifyMessage) error {
 	if option.Config.DryMode {
 		return nil
 	}
-	event := monitorAPI.AgentNotify{Type: typ, Text: text}
-	return d.monitorAgent.SendEvent(monitorAPI.MessageTypeAgent, event)
+	return d.monitorAgent.SendEvent(monitorAPI.MessageTypeAgent, notification)
 }
 
 // GetNodeSuffix returns the suffix to be appended to kvstore keys of this
diff --git a/daemon/cmd/daemon_main.go b/daemon/cmd/daemon_main.go
index 248c6c9510..38b4ced626 100644
--- a/daemon/cmd/daemon_main.go
+++ b/daemon/cmd/daemon_main.go
@@ -520,6 +520,9 @@ func init() {
 	flags.Bool(option.EnableSVCSourceRangeCheck, true, "Enable check of service source ranges (currently, only for LoadBalancer)")
 	option.BindEnv(option.EnableSVCSourceRangeCheck)
 
+	flags.Bool(option.EnableBandwidthManager, false, "Enable BPF bandwidth manager")
+	option.BindEnv(option.EnableBandwidthManager)
+
 	flags.String(option.NodePortMode, option.NodePortModeSNAT, "BPF NodePort mode (\"snat\", \"dsr\", \"hybrid\")")
 	option.BindEnv(option.NodePortMode)
 
@@ -1393,11 +1396,9 @@ func runDaemon() {
 	srv.ConfigureAPI()
 	bootstrapStats.initAPI.End(true)
 
-	repr, err := monitorAPI.TimeRepr(time.Now())
+	err = d.SendNotification(monitorAPI.StartMessage(time.Now()))
 	if err != nil {
-		log.WithError(err).Warn("Failed to generate agent start monitor message")
-	} else {
-		d.SendNotification(monitorAPI.AgentNotifyStart, repr)
+		log.WithError(err).Warn("Failed to send agent start monitor message")
 	}
 
 	log.WithField("bootstrapTime", time.Since(bootstrapTimestamp)).
diff --git a/daemon/cmd/endpoint.go b/daemon/cmd/endpoint.go
index b9f094d25e..a98e20155a 100644
--- a/daemon/cmd/endpoint.go
+++ b/daemon/cmd/endpoint.go
@@ -26,6 +26,7 @@ import (
 	. "github.com/cilium/cilium/api/v1/server/restapi/endpoint"
 	"github.com/cilium/cilium/pkg/annotation"
 	"github.com/cilium/cilium/pkg/api"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/endpoint"
 	endpointid "github.com/cilium/cilium/pkg/endpoint/id"
 	"github.com/cilium/cilium/pkg/endpoint/regeneration"
@@ -371,7 +372,7 @@ func (d *Daemon) createEndpoint(ctx context.Context, owner regeneration.Owner, e
 	defer d.endpointCreations.EndCreateRequest(ep)
 
 	if ep.K8sNamespaceAndPodNameIsSet() && k8s.IsEnabled() {
-		pod, cp, identityLabels, info, _, err := d.fetchK8sLabelsAndAnnotations(ep.K8sNamespace, ep.K8sPodName)
+		pod, cp, identityLabels, info, annotations, err := d.fetchK8sLabelsAndAnnotations(ep.K8sNamespace, ep.K8sPodName)
 		if err != nil {
 			ep.Logger("api").WithError(err).Warning("Unable to fetch kubernetes labels")
 		} else {
@@ -381,6 +382,20 @@ func (d *Daemon) createEndpoint(ctx context.Context, owner regeneration.Owner, e
 			}
 			addLabels.MergeLabels(identityLabels)
 			infoLabels.MergeLabels(info)
+			if _, ok := annotations[bandwidth.IngressBandwidth]; ok {
+				log.WithFields(logrus.Fields{
+					logfields.K8sPodName:  epTemplate.K8sNamespace + "/" + epTemplate.K8sPodName,
+					logfields.Annotations: logfields.Repr(annotations),
+				}).Warningf("Endpoint has %s annotation which is unsupported. This annotation is ignored.",
+					bandwidth.IngressBandwidth)
+			}
+			if _, ok := annotations[bandwidth.EgressBandwidth]; ok && !option.Config.EnableBandwidthManager {
+				log.WithFields(logrus.Fields{
+					logfields.K8sPodName:  epTemplate.K8sNamespace + "/" + epTemplate.K8sPodName,
+					logfields.Annotations: logfields.Repr(annotations),
+				}).Warningf("Endpoint has %s annotation, but BPF bandwidth manager is disabled. This annotation is ignored.",
+					bandwidth.EgressBandwidth)
+			}
 		}
 	}
 
@@ -430,9 +445,15 @@ func (d *Daemon) createEndpoint(ctx context.Context, owner regeneration.Owner, e
 			if err != nil {
 				return "", err
 			}
-
 			return p.Annotations[annotation.ProxyVisibility], nil
 		})
+		ep.UpdateBandwidthPolicy(func(ns, podName string) (bandwidthEgress string, err error) {
+			p, err := d.k8sWatcher.GetCachedPod(ns, podName)
+			if err != nil {
+				return "", err
+			}
+			return p.Annotations[bandwidth.EgressBandwidth], nil
+		})
 	}
 
 	regenTriggered := ep.UpdateLabels(ctx, addLabels, infoLabels, true)
@@ -614,11 +635,7 @@ func (d *Daemon) deleteEndpoint(ep *endpoint.Endpoint) int {
 
 // NotifyMonitorDeleted notifies the monitor that an endpoint has been deleted.
 func (d *Daemon) NotifyMonitorDeleted(ep *endpoint.Endpoint) {
-	repr, err := monitorAPI.EndpointDeleteRepr(ep)
-	// Ignore endpoint deletion if EndpointDeleteRepr != nil
-	if err == nil {
-		d.SendNotification(monitorAPI.AgentNotifyEndpointDeleted, repr)
-	}
+	d.SendNotification(monitorAPI.EndpointDeleteMessage(ep))
 }
 
 // deleteEndpointQuiet sets the endpoint into disconnecting state and removes
diff --git a/daemon/cmd/kube_proxy_replacement.go b/daemon/cmd/kube_proxy_replacement.go
index 4fbf4b50c6..78cef11291 100644
--- a/daemon/cmd/kube_proxy_replacement.go
+++ b/daemon/cmd/kube_proxy_replacement.go
@@ -233,11 +233,10 @@ func initKubeProxyReplacementOptions() (strict bool) {
 	return
 }
 
-// detectDevicesForNodePortAndHostFirewall tries to detect bpf_host devices
-// (if needed).
-func detectDevicesForNodePortAndHostFirewall(strict bool) {
+// detectNativeDevices tries to detect bpf_host devices (if needed).
+func detectNativeDevices(strict bool) {
 	detectNodePortDevs := len(option.Config.Devices) == 0 &&
-		(option.Config.EnableNodePort || option.Config.EnableHostFirewall)
+		(option.Config.EnableNodePort || option.Config.EnableHostFirewall || option.Config.EnableBandwidthManager)
 	detectDirectRoutingDev := option.Config.EnableNodePort &&
 		option.Config.DirectRoutingDevice == ""
 	if detectNodePortDevs || detectDirectRoutingDev {
diff --git a/daemon/cmd/policy.go b/daemon/cmd/policy.go
index 9950aaf8e2..b78120d079 100644
--- a/daemon/cmd/policy.go
+++ b/daemon/cmd/policy.go
@@ -271,6 +271,7 @@ func (d *Daemon) policyAdd(sourceRules policyAPI.Rules, opts *policy.AddOptions,
 	newPrefixLengths, err := d.prefixLengths.Add(prefixes)
 	if err != nil {
 		metrics.PolicyImportErrors.Inc()
+		metrics.PolicyImportErrorsTotal.Inc()
 		logger.WithError(err).WithField("prefixes", prefixes).Warn(
 			"Failed to reference-count prefix lengths in CIDR policy")
 		resChan <- &PolicyAddResult{
@@ -285,6 +286,7 @@ func (d *Daemon) policyAdd(sourceRules policyAPI.Rules, opts *policy.AddOptions,
 		if err := d.Datapath().Loader().Reinitialize(d.ctx, d, d.mtuConfig.GetDeviceMTU(), d.Datapath(), d.l7Proxy, d.ipam); err != nil {
 			_ = d.prefixLengths.Delete(prefixes)
 			metrics.PolicyImportErrors.Inc()
+			metrics.PolicyImportErrorsTotal.Inc()
 			err2 := fmt.Errorf("Unable to recompile base programs: %s", err)
 			logger.WithError(err2).WithField("prefixes", prefixes).Warn(
 				"Failed to recompile base programs due to prefix length count change")
@@ -299,6 +301,7 @@ func (d *Daemon) policyAdd(sourceRules policyAPI.Rules, opts *policy.AddOptions,
 	if _, err := ipcache.AllocateCIDRs(prefixes); err != nil {
 		_ = d.prefixLengths.Delete(prefixes)
 		metrics.PolicyImportErrors.Inc()
+		metrics.PolicyImportErrorsTotal.Inc()
 		logger.WithError(err).WithField("prefixes", prefixes).Warn(
 			"Failed to allocate identities for CIDRs during policy add")
 		resChan <- &PolicyAddResult{
@@ -401,11 +404,9 @@ func (d *Daemon) policyAdd(sourceRules policyAPI.Rules, opts *policy.AddOptions,
 	for _, r := range sourceRules {
 		labels = append(labels, r.Labels.GetModel()...)
 	}
-	repr, err := monitorAPI.PolicyUpdateRepr(len(sourceRules), labels, newRev)
+	err = d.SendNotification(monitorAPI.PolicyUpdateMessage(len(sourceRules), labels, newRev))
 	if err != nil {
-		logger.WithField(logfields.PolicyRevision, newRev).Warn("Failed to represent policy update as monitor notification")
-	} else {
-		d.SendNotification(monitorAPI.AgentNotifyPolicyUpdated, repr)
+		logger.WithField(logfields.PolicyRevision, newRev).Warn("Failed to send policy update as monitor notification")
 	}
 
 	if option.Config.SelectiveRegeneration {
@@ -632,11 +633,9 @@ func (d *Daemon) policyDelete(labels labels.LabelArray, res chan interface{}) {
 		d.TriggerPolicyUpdates(true, "policy rules deleted")
 	}
 
-	repr, err := monitorAPI.PolicyDeleteRepr(deleted, labels.GetModel(), rev)
+	err := d.SendNotification(monitorAPI.PolicyDeleteMessage(deleted, labels.GetModel(), rev))
 	if err != nil {
-		log.WithField(logfields.PolicyRevision, rev).Warn("Failed to represent policy update as monitor notification")
-	} else {
-		d.SendNotification(monitorAPI.AgentNotifyPolicyDeleted, repr)
+		log.WithField(logfields.PolicyRevision, rev).Warn("Failed to send policy update as monitor notification")
 	}
 
 	return
diff --git a/daemon/cmd/status.go b/daemon/cmd/status.go
index 248435fa5f..8562e4b39d 100644
--- a/daemon/cmd/status.go
+++ b/daemon/cmd/status.go
@@ -141,6 +141,24 @@ func (d *Daemon) getMasqueradingStatus() *models.Masquerading {
 	return s
 }
 
+func (d *Daemon) getBandwidthManagerStatus() *models.BandwidthManager {
+	s := &models.BandwidthManager{
+		Enabled: option.Config.EnableBandwidthManager,
+	}
+
+	if !option.Config.EnableBandwidthManager {
+		return s
+	}
+
+	devices := make([]string, len(option.Config.Devices))
+	for i, iface := range option.Config.Devices {
+		devices[i] = iface
+	}
+
+	s.Devices = devices
+	return s
+}
+
 func (d *Daemon) getKubeProxyReplacementStatus() *models.KubeProxyReplacement {
 	if !k8s.IsEnabled() {
 		return &models.KubeProxyReplacement{Mode: models.KubeProxyReplacementModeDisabled}
@@ -830,6 +848,7 @@ func (d *Daemon) startStatusCollector() {
 	}
 
 	d.statusResponse.Masquerading = d.getMasqueradingStatus()
+	d.statusResponse.BandwidthManager = d.getBandwidthManagerStatus()
 	d.statusResponse.BpfMaps = d.getBPFMapStatus()
 
 	d.statusCollector = status.NewCollector(probes, status.Config{})
diff --git a/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-dashboard.json b/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-dashboard.json
index 2d6d9c2fe9..c39b35c225 100644
--- a/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-dashboard.json
+++ b/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-dashboard.json
@@ -1866,10 +1866,10 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "topk(5, avg(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, mapName, operation))",
+          "expr": "topk(5, avg(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, map_name, operation))",
           "format": "time_series",
           "intervalFactor": 1,
-          "legendFormat": "{{mapName}} {{operation}}",
+          "legendFormat": "{{map_name}} {{operation}}",
           "refId": "A"
         }
       ],
@@ -1967,10 +1967,10 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "topk(5, max(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, mapName, operation))",
+          "expr": "topk(5, max(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, map_name, operation))",
           "format": "time_series",
           "intervalFactor": 1,
-          "legendFormat": "{{mapName}} {{operation}}",
+          "legendFormat": "{{map_name}} {{operation}}",
           "refId": "A"
         }
       ],
@@ -2068,10 +2068,10 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "sum(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\",outcome=\"fail\"}[5m])) by (pod, mapName, operation)",
+          "expr": "sum(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\",outcome=\"fail\"}[5m])) by (pod, map_name, operation)",
           "format": "time_series",
           "intervalFactor": 1,
-          "legendFormat": "{{mapName}} {{operation}}",
+          "legendFormat": "{{map_name}} {{operation}}",
           "refId": "A"
         }
       ],
@@ -4120,7 +4120,7 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "avg(rate(cilium_nodes_all_events_received_total{k8s_app=\"cilium\"}[1m])) by (pod, eventType, source) * 60",
+          "expr": "avg(rate(cilium_nodes_all_events_received_total{k8s_app=\"cilium\"}[1m])) by (pod, event_type, source) * 60",
           "format": "time_series",
           "intervalFactor": 1,
           "legendFormat": "{{eventType}} {{source}}",
@@ -5558,28 +5558,28 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "min(cilium_policy_count{k8s_app=\"cilium\"}) by(pod)",
+          "expr": "min(cilium_policy{k8s_app=\"cilium\"}) by(pod)",
           "format": "time_series",
           "intervalFactor": 1,
           "legendFormat": "min",
           "refId": "A"
         },
         {
-          "expr": "avg(cilium_policy_count{k8s_app=\"cilium\"}) by(pod)",
+          "expr": "avg(cilium_policy{k8s_app=\"cilium\"}) by(pod)",
           "format": "time_series",
           "intervalFactor": 1,
           "legendFormat": "avg",
           "refId": "B"
         },
         {
-          "expr": "max(cilium_policy_count{k8s_app=\"cilium\"}) by(pod)",
+          "expr": "max(cilium_policy{k8s_app=\"cilium\"}) by(pod)",
           "format": "time_series",
           "intervalFactor": 1,
           "legendFormat": "max",
           "refId": "C"
         },
         {
-          "expr": "sum(cilium_policy_import_errors{k8s_app=\"cilium\"}) by (pod)",
+          "expr": "sum(cilium_policy_import_errors_total{k8s_app=\"cilium\"}) by (pod)",
           "format": "time_series",
           "intervalFactor": 1,
           "legendFormat": "policy import errors",
@@ -6143,7 +6143,7 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "sum(rate(cilium_endpoint_regenerations{k8s_app=\"cilium\"}[30s])) by(outcome)",
+          "expr": "sum(rate(cilium_endpoint_regenerations_total{k8s_app=\"cilium\"}[30s])) by(outcome)",
           "format": "time_series",
           "instant": false,
           "intervalFactor": 1,
@@ -6928,7 +6928,7 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "sum(rate(cilium_k8s_client_api_calls_counter{k8s_app=\"cilium\"}[1m])) by (pod, method, return_code)",
+          "expr": "sum(rate(cilium_k8s_client_api_calls_total{k8s_app=\"cilium\"}[1m])) by (pod, method, return_code)",
           "format": "time_series",
           "intervalFactor": 1,
           "legendFormat": "{{method}} {{return_code}}",
diff --git a/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-operator-dashboard.json b/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-operator-dashboard.json
index f5998d24d0..1d59888787 100644
--- a/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-operator-dashboard.json
+++ b/examples/kubernetes/addons/prometheus/files/grafana-dashboards/cilium-operator-dashboard.json
@@ -425,7 +425,7 @@
           "expr": "rate(cilium_operator_eni_aws_api_duration_seconds_sum[1m])/rate(cilium_operator_eni_aws_api_duration_seconds_count[1m])",
           "format": "time_series",
           "intervalFactor": 1,
-          "legendFormat": "{{operation}} {{responseCode}}",
+          "legendFormat": "{{operation}} {{response_code}}",
           "refId": "A"
         }
       ],
diff --git a/examples/kubernetes/addons/prometheus/monitoring-example.yaml b/examples/kubernetes/addons/prometheus/monitoring-example.yaml
index b0c5555cc5..0b365dec0f 100644
--- a/examples/kubernetes/addons/prometheus/monitoring-example.yaml
+++ b/examples/kubernetes/addons/prometheus/monitoring-example.yaml
@@ -2218,10 +2218,10 @@ data:
           "steppedLine": false,
           "targets": [
             {
-              "expr": "topk(5, avg(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, mapName, operation))",
+              "expr": "topk(5, avg(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, map_name, operation))",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "{{mapName}} {{operation}}",
+              "legendFormat": "{{map_name}} {{operation}}",
               "refId": "A"
             }
           ],
@@ -2319,10 +2319,10 @@ data:
           "steppedLine": false,
           "targets": [
             {
-              "expr": "topk(5, max(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, mapName, operation))",
+              "expr": "topk(5, max(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\"}[5m])) by (pod, map_name, operation))",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "{{mapName}} {{operation}}",
+              "legendFormat": "{{map_name}} {{operation}}",
               "refId": "A"
             }
           ],
@@ -2420,10 +2420,10 @@ data:
           "steppedLine": false,
           "targets": [
             {
-              "expr": "sum(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\",outcome=\"fail\"}[5m])) by (pod, mapName, operation)",
+              "expr": "sum(rate(cilium_bpf_map_ops_total{k8s_app=\"cilium\",outcome=\"fail\"}[5m])) by (pod, map_name, operation)",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "{{mapName}} {{operation}}",
+              "legendFormat": "{{map_name}} {{operation}}",
               "refId": "A"
             }
           ],
@@ -4472,10 +4472,10 @@ data:
           "steppedLine": false,
           "targets": [
             {
-              "expr": "avg(rate(cilium_nodes_all_events_received_total{k8s_app=\"cilium\"}[1m])) by (pod, eventType, source) * 60",
+              "expr": "avg(rate(cilium_nodes_all_events_received_total{k8s_app=\"cilium\"}[1m])) by (pod, event_type, source) * 60",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "{{eventType}} {{source}}",
+              "legendFormat": "{{event_type}} {{source}}",
               "refId": "B"
             }
           ],
@@ -5910,28 +5910,28 @@ data:
           "steppedLine": false,
           "targets": [
             {
-              "expr": "min(cilium_policy_count{k8s_app=\"cilium\"}) by(pod)",
+              "expr": "min(cilium_policy{k8s_app=\"cilium\"}) by(pod)",
               "format": "time_series",
               "intervalFactor": 1,
               "legendFormat": "min",
               "refId": "A"
             },
             {
-              "expr": "avg(cilium_policy_count{k8s_app=\"cilium\"}) by(pod)",
+              "expr": "avg(cilium_policy{k8s_app=\"cilium\"}) by(pod)",
               "format": "time_series",
               "intervalFactor": 1,
               "legendFormat": "avg",
               "refId": "B"
             },
             {
-              "expr": "max(cilium_policy_count{k8s_app=\"cilium\"}) by(pod)",
+              "expr": "max(cilium_policy{k8s_app=\"cilium\"}) by(pod)",
               "format": "time_series",
               "intervalFactor": 1,
               "legendFormat": "max",
               "refId": "C"
             },
             {
-              "expr": "sum(cilium_policy_import_errors{k8s_app=\"cilium\"}) by (pod)",
+              "expr": "sum(cilium_policy_import_errors_total{k8s_app=\"cilium\"}) by (pod)",
               "format": "time_series",
               "intervalFactor": 1,
               "legendFormat": "policy import errors",
@@ -6495,7 +6495,7 @@ data:
           "steppedLine": false,
           "targets": [
             {
-              "expr": "sum(rate(cilium_endpoint_regenerations{k8s_app=\"cilium\"}[30s])) by(outcome)",
+              "expr": "sum(rate(cilium_endpoint_regenerations_total{k8s_app=\"cilium\"}[30s])) by(outcome)",
               "format": "time_series",
               "instant": false,
               "intervalFactor": 1,
@@ -7280,7 +7280,7 @@ data:
           "steppedLine": false,
           "targets": [
             {
-              "expr": "sum(rate(cilium_k8s_client_api_calls_counter{k8s_app=\"cilium\"}[1m])) by (pod, method, return_code)",
+              "expr": "sum(rate(cilium_k8s_client_api_calls_total{k8s_app=\"cilium\"}[1m])) by (pod, method, return_code)",
               "format": "time_series",
               "intervalFactor": 1,
               "legendFormat": "{{method}} {{return_code}}",
@@ -8936,7 +8936,7 @@ data:
               "expr": "rate(cilium_operator_eni_aws_api_duration_seconds_sum[1m])/rate(cilium_operator_eni_aws_api_duration_seconds_count[1m])",
               "format": "time_series",
               "intervalFactor": 1,
-              "legendFormat": "{{operation}} {{responseCode}}",
+              "legendFormat": "{{operation}} {{response_code}}",
               "refId": "A"
             }
           ],
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml b/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
index 7b8c2f3701..8169aa18a9 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -184,8 +190,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-c-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40001
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -199,7 +208,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -210,7 +219,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-c
@@ -316,7 +325,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -327,7 +336,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -420,14 +429,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -470,7 +479,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -481,7 +490,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -578,7 +587,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -589,7 +598,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-intra-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -604,7 +613,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -657,7 +666,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -668,7 +677,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-multi-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -683,7 +692,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -736,7 +745,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -747,7 +756,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -762,7 +771,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -815,7 +824,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -826,7 +835,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -841,7 +850,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -894,7 +903,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -905,7 +914,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -920,7 +929,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -973,7 +982,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -984,7 +993,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -999,7 +1008,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1052,7 +1061,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -1063,7 +1072,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1116,7 +1125,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -1127,7 +1136,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1180,7 +1189,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -1191,7 +1200,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1245,7 +1254,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -1256,7 +1265,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -1542,7 +1551,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -1560,7 +1570,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -1579,7 +1590,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -1597,7 +1609,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -1634,7 +1647,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -1709,7 +1723,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
@@ -1790,7 +1804,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1834,7 +1848,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1878,7 +1892,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1922,7 +1936,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -1966,7 +1980,7 @@ spec:
   ingress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml b/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml
index c0b0a2fefc..3080249160 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-internal.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -201,7 +207,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -212,7 +218,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -251,14 +257,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -301,7 +307,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -312,7 +318,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -355,7 +361,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -366,7 +372,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -419,7 +425,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -430,7 +436,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -483,7 +489,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -494,7 +500,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -548,7 +554,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -559,7 +565,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -717,7 +723,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -735,7 +742,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -754,7 +762,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -829,7 +838,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml b/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml
index 1349b06a41..e606942579 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-proxy.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-c-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40001
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -33,7 +36,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -44,7 +47,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-c
@@ -150,7 +153,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -161,7 +164,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-intra-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -176,7 +179,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -229,7 +232,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -240,7 +243,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
       - name: pod-to-a-multi-node-proxy-egress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -255,7 +258,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -308,7 +311,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -319,7 +322,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -334,7 +337,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -387,7 +390,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -398,7 +401,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-ingress-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -413,7 +416,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -466,7 +469,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -477,7 +480,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-intra-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -492,7 +495,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -545,7 +548,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
         livenessProbe:
           exec:
             command:
@@ -556,7 +559,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-c/public
+            - echo-c:8080/public
       - name: pod-to-c-multi-node-proxy-to-proxy-policy-reject-container
         ports: []
         image: docker.io/byrnedo/alpine-curl:0.1.8
@@ -571,7 +574,7 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-c:8080/private'
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -600,7 +603,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -618,7 +622,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-c
@@ -660,7 +665,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -704,7 +709,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -748,7 +753,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -792,7 +797,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -836,7 +841,7 @@ spec:
   ingress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
       rules:
         http:
@@ -865,8 +870,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -879,7 +887,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -890,7 +898,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -909,7 +917,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -935,8 +944,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -950,7 +962,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -961,7 +973,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -980,7 +992,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml b/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml
index e9753583fa..776fab622b 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-quarantine.yaml
@@ -658,8 +658,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -672,7 +675,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -683,7 +686,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -702,7 +705,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -728,8 +732,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -743,7 +750,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -754,7 +761,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -773,7 +780,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml b/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
index 30c897023a..2c377c5070 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -201,7 +207,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -212,7 +218,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -305,14 +311,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -355,7 +361,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -366,7 +372,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -503,7 +509,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -521,7 +528,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -540,7 +548,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -615,7 +624,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check.yaml b/examples/kubernetes/connectivity-check/connectivity-check.yaml
index dd11d3dffd..4e93187069 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check.yaml
@@ -18,8 +18,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-a-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
         readinessProbe:
@@ -32,7 +35,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -43,7 +46,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-a
@@ -69,8 +72,11 @@ spec:
       hostNetwork: false
       containers:
       - name: echo-b-container
+        env:
+        - name: PORT
+          value: "8080"
         ports:
-        - containerPort: 80
+        - containerPort: 8080
           hostPort: 40000
         image: docker.io/cilium/json-mock:1.2
         imagePullPolicy: IfNotPresent
@@ -84,7 +90,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
         livenessProbe:
           exec:
             command:
@@ -95,7 +101,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - localhost
+            - localhost:8080
   selector:
     matchLabels:
       name: echo-b
@@ -201,7 +207,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -212,7 +218,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a
@@ -305,14 +311,14 @@ spec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
         livenessProbe:
           timeoutSeconds: 7
           exec:
             command:
             - ash
             - -c
-            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a/private'
+            - '! curl -s --fail --connect-timeout 5 -o /dev/null echo-a:8080/private'
   selector:
     matchLabels:
       name: pod-to-a-denied-cnp
@@ -355,7 +361,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
         livenessProbe:
           exec:
             command:
@@ -366,7 +372,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-a/public
+            - echo-a:8080/public
   selector:
     matchLabels:
       name: pod-to-a-allowed-cnp
@@ -463,7 +469,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -474,7 +480,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -527,7 +533,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -538,7 +544,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -591,7 +597,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
         livenessProbe:
           exec:
             command:
@@ -602,7 +608,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b/public
+            - echo-b:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -656,7 +662,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
         livenessProbe:
           exec:
             command:
@@ -667,7 +673,7 @@ spec:
             - "5"
             - -o
             - /dev/null
-            - echo-b-headless/public
+            - echo-b-headless:8080/public
       affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
@@ -825,7 +831,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-a
@@ -843,7 +850,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
     nodePort: 31313
   type: NodePort
   selector:
@@ -862,7 +870,8 @@ metadata:
     type: autocheck
 spec:
   ports:
-  - port: 80
+  - name: http
+    port: 8080
   type: ClusterIP
   selector:
     name: echo-b
@@ -937,7 +946,7 @@ spec:
   egress:
   - toPorts:
     - ports:
-      - port: "80"
+      - port: "8080"
         protocol: TCP
     toEndpoints:
     - matchLabels:
diff --git a/examples/kubernetes/connectivity-check/defaults.cue b/examples/kubernetes/connectivity-check/defaults.cue
index d017abc446..b0d9c9282f 100644
--- a/examples/kubernetes/connectivity-check/defaults.cue
+++ b/examples/kubernetes/connectivity-check/defaults.cue
@@ -10,13 +10,13 @@ deployment: [ID=_]: {
 
 	// readinessProbe target name
 	if ID =~ "^pod-to-a.*$" || ID =~ "^host-to-a.*$" {
-		_probeTarget: *"echo-a" | string
+		_probeTarget: *"echo-a:8080" | string
 	}
 	if ID =~ "^pod-to-b.*$" || ID =~ "^host-to-b.*$" {
-		_probeTarget: *"echo-b" | string
+		_probeTarget: *"echo-b:8080" | string
 	}
 	if ID =~ "^pod-to-c.*$" || ID =~ "^host-to-c.*$" {
-		_probeTarget: *"echo-c" | string
+		_probeTarget: *"echo-c:8080" | string
 	}
 }
 
diff --git a/examples/kubernetes/connectivity-check/echo-servers.cue b/examples/kubernetes/connectivity-check/echo-servers.cue
index 77b698dfc7..9f75f82f6a 100644
--- a/examples/kubernetes/connectivity-check/echo-servers.cue
+++ b/examples/kubernetes/connectivity-check/echo-servers.cue
@@ -3,7 +3,7 @@ package connectivity_check
 // Default parameters for echo servers (may be overridden).
 _echoDeployment: {
 	_image:       "docker.io/cilium/json-mock:1.2"
-	_probeTarget: *"localhost" | string
+	_probeTarget: *"localhost:8080" | string
 	_probePath:   ""
 }
 
@@ -15,19 +15,21 @@ _echoDeploymentWithHostPort: _echoDeployment & {
 
 // Regular service exposed via ClusterIP.
 deployment: "echo-a": _echoDeployment & {
+	_serverPort: "8080"
 	_exposeClusterIP: true
 	metadata: labels: component: "network-check"
-	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 80}]}]
+	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 8080, _portName: "http"}]}]
 }
 
 // Service exposed via NodePort + headless svc.
 deployment: "echo-b": _echoDeployment & {
+	_serverPort: "8080"
 	_exposeNodePort: true
 	_exposeHeadless: true
 	_nodePort:       31313
 
 	metadata: labels: component: "services-check"
-	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 80, hostPort: 40000}]}]
+	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 8080, _portName: "http", hostPort: 40000}]}]
 }
 // Expose hostport by deploying a host pod and adding a headless service with no port.
 deployment: "echo-b-host": _echoDeploymentWithHostPort & {
@@ -39,7 +41,7 @@ deployment: "echo-b-host": _echoDeploymentWithHostPort & {
 
 ingressL7Policy: {
 	_allowDNS: true
-	_port:     *"80" | string
+	_port:     *"8080" | string
 	_rules: [{
 		toPorts: [{
 			ports: [{
@@ -59,11 +61,12 @@ ingressL7Policy: {
 
 // Service with policy applied.
 deployment: "echo-c": _echoDeployment & {
+	_serverPort: "8080"
 	_exposeClusterIP: true
 	_exposeHeadless:  true
 
 	metadata: labels: component: "proxy-check"
-	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 80, hostPort: 40001}]}]
+	spec: template: spec: containers: [{ports: [{_expose: true, containerPort: 8080, _portName: "http", hostPort: 40001}]}]
 }
 ingressCNP: "echo-c": ingressL7Policy & {}
 // Expose hostport by deploying a host pod and adding a headless service with no port.
diff --git a/examples/kubernetes/connectivity-check/policy.cue b/examples/kubernetes/connectivity-check/policy.cue
index 01e6141735..a58cce7780 100644
--- a/examples/kubernetes/connectivity-check/policy.cue
+++ b/examples/kubernetes/connectivity-check/policy.cue
@@ -28,7 +28,7 @@ egressCNP: "pod-to-a-allowed-cnp":  _policyResource & {
 		}]
 		toPorts: [{
 			ports: [{
-				port:     "80"
+				port:     "8080"
 				protocol: "TCP"
 			}]
 		}]
diff --git a/examples/kubernetes/connectivity-check/proxy.cue b/examples/kubernetes/connectivity-check/proxy.cue
index d6424aabea..075a975c84 100644
--- a/examples/kubernetes/connectivity-check/proxy.cue
+++ b/examples/kubernetes/connectivity-check/proxy.cue
@@ -13,7 +13,7 @@ _proxyResource: {
 _egressL7Policy: {
 	_allowDNS: true
 
-	_port:   *"80" | string
+	_port:   *"8080" | string
 	_target: *"" | string
 	_rules: [{
 		if _target != "" {
diff --git a/examples/kubernetes/connectivity-check/resources.cue b/examples/kubernetes/connectivity-check/resources.cue
index 3d85dd0486..40d78359ca 100644
--- a/examples/kubernetes/connectivity-check/resources.cue
+++ b/examples/kubernetes/connectivity-check/resources.cue
@@ -273,6 +273,7 @@ for x in [deployment] for k, v in x {
 				if p._expose {
 					let Port = p.containerPort // Port is an alias
 					port: *Port | int
+					name: p._portName
 					if v._exposeNodePort {
 						nodePort: v._nodePort
 					}
@@ -300,6 +301,7 @@ for x in [deployment] for k, v in x {
 				if p._expose {
 					let Port = p.containerPort // Port is an alias
 					port: *Port | int
+					name: p._portName
 				},
 			]
 		}
diff --git a/examples/kubernetes/connectivity-check/services.cue b/examples/kubernetes/connectivity-check/services.cue
index ce502f86bc..91b1478d68 100644
--- a/examples/kubernetes/connectivity-check/services.cue
+++ b/examples/kubernetes/connectivity-check/services.cue
@@ -2,7 +2,7 @@ package connectivity_check
 
 deployment: [ID=_]: {
 	if ID =~ "^[-_a-zA-Z0-9]*-headless$" {
-		_probeTarget: "echo-b-headless"
+		_probeTarget: "echo-b-headless:8080"
 	}
 }
 
diff --git a/install/kubernetes/cilium/charts/config/templates/configmap.yaml b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
index e2f228d197..eae40a6a8f 100644
--- a/install/kubernetes/cilium/charts/config/templates/configmap.yaml
+++ b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
@@ -400,6 +400,7 @@ data:
   iptables-lock-timeout: {{ .Values.global.iptablesLockTimeout | quote }}
 {{- end }}
   auto-direct-node-routes: {{ .Values.global.autoDirectNodeRoutes | quote }}
+  enable-bandwidth-manager: {{ .Values.global.bandwidthManager | quote }}
 {{- if .Values.global.nativeRoutingCIDR }}
   native-routing-cidr: {{ .Values.global.nativeRoutingCIDR }}
 {{- end }}
@@ -468,6 +469,9 @@ data:
 {{- else if eq $defaultSessionAffinity "true" }}
   enable-session-affinity: {{ $defaultSessionAffinity | quote }}
 {{- end }}
+{{- if hasKey .Values "svcSourceRangeCheck" }}
+  enable-svc-source-range-check: {{ .Values.svcSourceRangeCheck | quote }}
+{{- end }}
 
 {{- if and .Values.global.pprof .Values.global.pprof.enabled }}
   pprof: {{ .Values.global.pprof.enabled | quote }}
diff --git a/install/kubernetes/cilium/charts/prometheus-crd/crds/crd-servicemonitor.yaml b/install/kubernetes/cilium/charts/prometheus-crd/crds/crd-servicemonitor.yaml
index e631c2c090..08483c5a96 100644
--- a/install/kubernetes/cilium/charts/prometheus-crd/crds/crd-servicemonitor.yaml
+++ b/install/kubernetes/cilium/charts/prometheus-crd/crds/crd-servicemonitor.yaml
@@ -5,7 +5,6 @@ metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.2.4
     helm.sh/hook: crd-install
-  creationTimestamp: null
   name: servicemonitors.monitoring.coreos.com
 spec:
   group: monitoring.coreos.com
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index 847b2b13e8..5da17a6448 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -335,6 +335,10 @@ global:
   # hostFirewall enables the enforcement of host policies in the BPF datapath
   hostFirewall: false
 
+  # bandwidthManager enables BBR and FQ + EDT + BPF based rate-limiting for
+  # Pods that are annotated with kubernetes.io/egress-bandwidth.
+  bandwidthManager: true
+
   # kubeProxyReplacement enables kube-proxy replacement in Cilium BPF datapath
   kubeProxyReplacement: "probe"
 
diff --git a/install/kubernetes/experimental-install.yaml b/install/kubernetes/experimental-install.yaml
index c5a984e5d8..c94fa95ea2 100644
--- a/install/kubernetes/experimental-install.yaml
+++ b/install/kubernetes/experimental-install.yaml
@@ -125,6 +125,7 @@ data:
   enable-xt-socket-fallback: "true"
   install-iptables-rules: "true"
   auto-direct-node-routes: "false"
+  enable-bandwidth-manager: "true"
   kube-proxy-replacement:  "probe"
   enable-health-check-nodeport: "true"
   node-port-bind-protection: "true"
diff --git a/install/kubernetes/quick-install.yaml b/install/kubernetes/quick-install.yaml
index a34b267659..6209c36cfb 100644
--- a/install/kubernetes/quick-install.yaml
+++ b/install/kubernetes/quick-install.yaml
@@ -111,6 +111,7 @@ data:
   enable-xt-socket-fallback: "true"
   install-iptables-rules: "true"
   auto-direct-node-routes: "false"
+  enable-bandwidth-manager: "true"
   kube-proxy-replacement:  "probe"
   enable-health-check-nodeport: "true"
   node-port-bind-protection: "true"
diff --git a/pkg/api/metrics/metrics.go b/pkg/api/metrics/metrics.go
index 3e7319f656..11e5210cb9 100644
--- a/pkg/api/metrics/metrics.go
+++ b/pkg/api/metrics/metrics.go
@@ -38,7 +38,7 @@ func NewPrometheusMetrics(namespace, subsystem string, registry *prometheus.Regi
 		Subsystem: subsystem,
 		Name:      "api_duration_seconds",
 		Help:      "Duration of interactions with API",
-	}, []string{"operation", "responseCode"})
+	}, []string{"operation", "responseCode", "response_code"}) //TODO(sayboras): Remove deprecated tag responseCode in 1.10
 
 	m.RateLimit = prometheus.NewHistogramVec(prometheus.HistogramOpts{
 		Namespace: namespace,
@@ -56,7 +56,8 @@ func NewPrometheusMetrics(namespace, subsystem string, registry *prometheus.Regi
 // ObserveAPICall must be called on every API call made with the operation
 // performed, the status code received and the duration of the call
 func (p *PrometheusMetrics) ObserveAPICall(operation, status string, duration float64) {
-	p.ApiDuration.WithLabelValues(operation, status).Observe(duration)
+	//TODO(sayboras): Remove deprecated tag responseCode in 1.10
+	p.ApiDuration.WithLabelValues(operation, status, status).Observe(duration)
 }
 
 // ObserveRateLimit must be called in case an API call was subject to rate limiting
diff --git a/pkg/bandwidth/bandwidth.go b/pkg/bandwidth/bandwidth.go
new file mode 100644
index 0000000000..74f1159f46
--- /dev/null
+++ b/pkg/bandwidth/bandwidth.go
@@ -0,0 +1,164 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bandwidth
+
+import (
+	"github.com/cilium/cilium/pkg/datapath/linux/probes"
+	"github.com/cilium/cilium/pkg/logging"
+	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+	"github.com/cilium/cilium/pkg/option"
+	"github.com/cilium/cilium/pkg/sysctl"
+	"github.com/sirupsen/logrus"
+	"github.com/vishvananda/netlink"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+const (
+	subsystem = "bandwidth-manager"
+
+	// EgressBandwidth is the K8s Pod annotation.
+	EgressBandwidth = "kubernetes.io/egress-bandwidth"
+	// IngressBandwidth is the K8s Pod annotation.
+	IngressBandwidth = "kubernetes.io/ingress-bandwidth"
+)
+
+var log = logging.DefaultLogger.WithField(logfields.LogSubsys, subsystem)
+
+func GetBytesPerSec(bandwidth string) (uint64, error) {
+	res, err := resource.ParseQuantity(bandwidth)
+	if err != nil {
+		return 0, err
+	}
+	return uint64(res.Value() / 8), err
+}
+
+func ProbeBandwidthManager() {
+	if option.Config.DryMode || !option.Config.EnableBandwidthManager {
+		return
+	}
+
+	if _, err := sysctl.Read("net.core.default_qdisc"); err != nil {
+		log.Warn("BPF bandwidth manager could not read procfs. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+
+	kernelGood := false
+	if h := probes.NewProbeManager().GetHelpers("sched_cls"); h != nil {
+		// We at least need 5.0 kernel for native TCP EDT integration.
+		// Below helper is available for 5.0 kernels and onwards.
+		if _, ok := h["bpf_sk_release"]; ok {
+			kernelGood = true
+		}
+	}
+	if !kernelGood {
+		log.Warn("BPF bandwidth manager needs kernel 5.0 or newer. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+}
+
+func InitBandwidthManager() {
+	if option.Config.DryMode || !option.Config.EnableBandwidthManager {
+		return
+	}
+
+	if len(option.Config.Devices) == 0 {
+		log.Warn("BPF bandwidth manager could not detect host devices. Disabling the feature.")
+		option.Config.EnableBandwidthManager = false
+		return
+	}
+
+	log.Info("Setting up BPF bandwidth manager")
+
+	if _, err := bwmap.ThrottleMap.OpenOrCreate(); err != nil {
+		log.WithError(err).Fatal("Failed to access ThrottleMap")
+	}
+
+	type setting struct {
+		name string
+		val  string
+	}
+	baseSettings := []setting{
+		{"net.core.netdev_max_backlog", "1000"},
+		{"net.core.somaxconn", "4096"},
+		{"net.core.default_qdisc", "fq"},
+		{"net.ipv4.tcp_congestion_control", "bbr"},
+		{"net.ipv4.tcp_max_syn_backlog", "4096"},
+	}
+	for _, s := range baseSettings {
+		log.WithFields(logrus.Fields{
+			logfields.SysParamName:  s.name,
+			logfields.SysParamValue: s.val,
+		}).Info("Setting sysctl")
+		if err := sysctl.Write(s.name, s.val); err != nil {
+			log.WithError(err).WithFields(logrus.Fields{
+				logfields.SysParamName:  s.name,
+				logfields.SysParamValue: s.val,
+			}).Fatal("Failed to set sysctl needed by BPF bandwidth manager.")
+		}
+	}
+
+	for _, device := range option.Config.Devices {
+		link, err := netlink.LinkByName(device)
+		if err != nil {
+			log.WithError(err).WithField("device", device).Warn("Link does not exist")
+			continue
+		}
+		// We strictly want to avoid a down/up cycle on the device at
+		// runtime, so given we've changed the default qdisc to FQ, we
+		// need to reset the root qdisc, and then set up MQ which will
+		// automatically get FQ leaf qdiscs (given it's been default).
+		qdisc := &netlink.GenericQdisc{
+			QdiscAttrs: netlink.QdiscAttrs{
+				LinkIndex: link.Attrs().Index,
+				Parent:    netlink.HANDLE_ROOT,
+			},
+			QdiscType: "noqueue",
+		}
+		if err := netlink.QdiscReplace(qdisc); err != nil {
+			log.WithError(err).WithField("device", device).
+				Fatalf("Cannot replace root Qdisc to %s", qdisc.QdiscType)
+		}
+		qdisc = &netlink.GenericQdisc{
+			QdiscAttrs: netlink.QdiscAttrs{
+				LinkIndex: link.Attrs().Index,
+				Parent:    netlink.HANDLE_ROOT,
+			},
+			QdiscType: "mq",
+		}
+		which := "mq with fq leafs"
+		if err := netlink.QdiscReplace(qdisc); err != nil {
+			// No MQ support, so just replace to FQ directly.
+			fq := &netlink.Fq{
+				QdiscAttrs: netlink.QdiscAttrs{
+					LinkIndex: link.Attrs().Index,
+					Parent:    netlink.HANDLE_ROOT,
+				},
+				Pacing: 1,
+			}
+			// At this point there is nothing we can do about
+			// it if we fail here, so hard bail out.
+			if err = netlink.QdiscReplace(fq); err != nil {
+				log.WithError(err).WithField("device", device).
+					Fatalf("Cannot replace root Qdisc to %s", fq.Type())
+			}
+			which = "fq"
+		}
+		log.WithField("device", device).Infof("Setting qdisc to %s", which)
+	}
+}
diff --git a/pkg/bandwidth/doc.go b/pkg/bandwidth/doc.go
new file mode 100644
index 0000000000..9dd2769508
--- /dev/null
+++ b/pkg/bandwidth/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package bandwidth provides efficient EDT-based rate-limiting.
+package bandwidth
diff --git a/pkg/bpf/map_linux.go b/pkg/bpf/map_linux.go
index 79c8b7d075..b29e0afaf7 100644
--- a/pkg/bpf/map_linux.go
+++ b/pkg/bpf/map_linux.go
@@ -842,7 +842,8 @@ func (m *Map) Update(key MapKey, value MapValue) error {
 
 	err = UpdateElement(m.fd, key.GetKeyPtr(), value.GetValuePtr(), 0)
 	if option.Config.MetricsConfig.BPFMapOps {
-		metrics.BPFMapOps.WithLabelValues(m.commonName(), metricOpUpdate, metrics.Error2Outcome(err)).Inc()
+		//TODO(sayboras): Remove deprecated label in 1.10
+		metrics.BPFMapOps.WithLabelValues(m.commonName(), m.commonName(), metricOpUpdate, metrics.Error2Outcome(err)).Inc()
 	}
 	return err
 }
@@ -890,7 +891,8 @@ func (m *Map) Delete(key MapKey) error {
 
 	_, errno := deleteElement(m.fd, key.GetKeyPtr())
 	if option.Config.MetricsConfig.BPFMapOps {
-		metrics.BPFMapOps.WithLabelValues(m.commonName(), metricOpDelete, metrics.Errno2Outcome(errno)).Inc()
+		//TODO(sayboras): Remove deprecated label in 1.10
+		metrics.BPFMapOps.WithLabelValues(m.commonName(), m.commonName(), metricOpDelete, metrics.Errno2Outcome(errno)).Inc()
 	}
 	if errno != 0 {
 		err = fmt.Errorf("unable to delete element %s from map %s: %w", key, m.name, errno)
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 819bcc613a..6607cfedc4 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -400,6 +400,17 @@ func FormatStatusResponse(w io.Writer, sr *models.StatusResponse, sd StatusDetai
 		}
 	}
 
+	if sr.BandwidthManager != nil {
+		var status string
+		if !sr.BandwidthManager.Enabled {
+			status = "Disabled"
+		} else {
+			status = fmt.Sprintf("EDT with BPF\t[%s]",
+				strings.Join(sr.BandwidthManager.Devices, ", "))
+		}
+		fmt.Fprintf(w, "BandwidthManager:\t%s\n", status)
+	}
+
 	if sr.Masquerading != nil {
 		var status string
 		if !sr.Masquerading.Enabled {
diff --git a/pkg/datapath/alignchecker/alignchecker.go b/pkg/datapath/alignchecker/alignchecker.go
index 6f9e3b1eff..f75deaddc8 100644
--- a/pkg/datapath/alignchecker/alignchecker.go
+++ b/pkg/datapath/alignchecker/alignchecker.go
@@ -19,6 +19,7 @@ import (
 
 	check "github.com/cilium/cilium/pkg/alignchecker"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 	"github.com/cilium/cilium/pkg/maps/eppolicymap"
 	"github.com/cilium/cilium/pkg/maps/eventsmap"
@@ -86,6 +87,8 @@ func CheckStructAlignments(path string) error {
 		"lb_affinity_val":   {reflect.TypeOf(lbmap.AffinityValue{})},
 		"lb4_src_range_key": {reflect.TypeOf(lbmap.SourceRangeKey4{})},
 		"lb6_src_range_key": {reflect.TypeOf(lbmap.SourceRangeKey6{})},
+		"edt_id":            {reflect.TypeOf(bwmap.EdtId{})},
+		"edt_info":          {reflect.TypeOf(bwmap.EdtInfo{})},
 	}
 	if err := check.CheckStructAlignments(path, toCheck, true); err != nil {
 		return err
diff --git a/pkg/datapath/ipcache/listener.go b/pkg/datapath/ipcache/listener.go
index 808339018c..0d83443090 100644
--- a/pkg/datapath/ipcache/listener.go
+++ b/pkg/datapath/ipcache/listener.go
@@ -47,7 +47,7 @@ type datapath interface {
 
 // monitor is an interface not notify the monitor about changes to the ipcache
 type monitorNotify interface {
-	SendNotification(typ monitorAPI.AgentNotification, text string) error
+	SendNotification(msg monitorAPI.AgentNotifyMessage) error
 }
 
 // BPFListener implements the ipcache.IPIdentityMappingBPFListener
@@ -105,15 +105,15 @@ func (l *BPFListener) notifyMonitor(modType ipcache.CacheModification,
 		oldIdentityPtr = &oldIdentity
 	}
 
-	repr, err := monitorAPI.IPCacheNotificationRepr(cidr.String(), newIdentity, oldIdentityPtr,
-		newHostIP, oldHostIP, encryptKey, k8sNamespace, k8sPodName)
-	if err == nil {
-		switch modType {
-		case ipcache.Upsert:
-			l.monitorNotify.SendNotification(monitorAPI.AgentNotifyIPCacheUpserted, repr)
-		case ipcache.Delete:
-			l.monitorNotify.SendNotification(monitorAPI.AgentNotifyIPCacheDeleted, repr)
-		}
+	switch modType {
+	case ipcache.Upsert:
+		msg := monitorAPI.IPCacheUpsertedMessage(cidr.String(), newIdentity, oldIdentityPtr,
+			newHostIP, oldHostIP, encryptKey, k8sNamespace, k8sPodName)
+		l.monitorNotify.SendNotification(msg)
+	case ipcache.Delete:
+		msg := monitorAPI.IPCacheDeletedMessage(cidr.String(), newIdentity, oldIdentityPtr,
+			newHostIP, oldHostIP, encryptKey, k8sNamespace, k8sPodName)
+		l.monitorNotify.SendNotification(msg)
 	}
 }
 
diff --git a/pkg/datapath/linux/config/config.go b/pkg/datapath/linux/config/config.go
index 1dc2e0c1bc..6926893205 100644
--- a/pkg/datapath/linux/config/config.go
+++ b/pkg/datapath/linux/config/config.go
@@ -33,6 +33,7 @@ import (
 	"github.com/cilium/cilium/pkg/defaults"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/labels"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
 	"github.com/cilium/cilium/pkg/maps/callsmap"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 	"github.com/cilium/cilium/pkg/maps/encrypt"
@@ -316,6 +317,12 @@ func (h *HeaderfileWriter) WriteNodeConfig(w io.Writer, cfg *datapath.LocalNodeC
 		}
 	}
 
+	if option.Config.EnableBandwidthManager {
+		cDefinesMap["ENABLE_BANDWIDTH_MANAGER"] = "1"
+		cDefinesMap["THROTTLE_MAP"] = bwmap.MapName
+		cDefinesMap["THROTTLE_MAP_SIZE"] = fmt.Sprintf("%d", bwmap.MapSize)
+	}
+
 	if option.Config.EnableHostFirewall {
 		cDefinesMap["ENABLE_HOST_FIREWALL"] = "1"
 	}
diff --git a/pkg/datapath/loader/base.go b/pkg/datapath/loader/base.go
index a19c56a51f..469bebf4a7 100644
--- a/pkg/datapath/loader/base.go
+++ b/pkg/datapath/loader/base.go
@@ -129,6 +129,7 @@ func (l *Loader) Reinitialize(ctx context.Context, o datapath.BaseProgramOwner,
 		{"net.core.bpf_jit_enable", "1", true},
 		{"net.ipv4.conf.all.rp_filter", "0", false},
 		{"kernel.unprivileged_bpf_disabled", "1", true},
+		{"kernel.timer_migration", "0", true},
 	}
 
 	// Lock so that endpoints cannot be built while we are compile base programs.
@@ -290,11 +291,16 @@ func (l *Loader) Reinitialize(ctx context.Context, o datapath.BaseProgramOwner,
 	args[initArgNrCPUs] = fmt.Sprintf("%d", common.GetNumPossibleCPUs(log))
 
 	clockSource := []string{"ktime", "jiffies"}
-	log.Infof("Setting up base BPF datapath (BPF %s instruction set, %s clock source)",
-		args[initBPFCPU], clockSource[option.Config.ClockSource])
+	log.WithFields(logrus.Fields{
+		logfields.BPFInsnSet:     args[initBPFCPU],
+		logfields.BPFClockSource: clockSource[option.Config.ClockSource],
+	}).Info("Setting up BPF datapath")
 
 	for _, s := range sysSettings {
-		log.Infof("Setting sysctl %s=%s", s.name, s.val)
+		log.WithFields(logrus.Fields{
+			logfields.SysParamName:  s.name,
+			logfields.SysParamValue: s.val,
+		}).Info("Setting sysctl")
 		if err := sysctl.Write(s.name, s.val); err != nil {
 			if !s.ignoreErr {
 				return fmt.Errorf("Failed to sysctl -w %s=%s: %s", s.name, s.val, err)
diff --git a/pkg/datapath/loader/cache.go b/pkg/datapath/loader/cache.go
index 618e1db614..8cacad8d56 100644
--- a/pkg/datapath/loader/cache.go
+++ b/pkg/datapath/loader/cache.go
@@ -58,6 +58,7 @@ var ignoredELFPrefixes = []string{
 	"cilium_tunnel",              // Global
 	"cilium_ipv4_frag_datagrams", // Global
 	"cilium_ipmasq",              // Global
+	"cilium_throttle",            // Global
 	"from-container",             // Prog name
 	"to-container",               // Prog name
 	"from-netdev",                // Prog name
diff --git a/pkg/datapath/maps/map.go b/pkg/datapath/maps/map.go
index 74928d03b9..6e0a7650c7 100644
--- a/pkg/datapath/maps/map.go
+++ b/pkg/datapath/maps/map.go
@@ -165,6 +165,10 @@ func (ms *MapSweeper) RemoveDisabledMaps() {
 		maps = append(maps, "cilium_ipv4_frag_datagrams")
 	}
 
+	if !option.Config.EnableBandwidthManager {
+		maps = append(maps, "cilium_throttle")
+	}
+
 	// Can be removed with Cilium 1.8
 	maps = append(maps, []string{
 		"cilium_policy_reserved_1",
diff --git a/pkg/endpoint/endpoint.go b/pkg/endpoint/endpoint.go
index 6860b69318..5301f7690d 100644
--- a/pkg/endpoint/endpoint.go
+++ b/pkg/endpoint/endpoint.go
@@ -31,6 +31,7 @@ import (
 	"github.com/cilium/cilium/api/v1/models"
 	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/annotation"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/controller"
@@ -160,6 +161,9 @@ type Endpoint struct {
 	// information of the endpoint has changed
 	identityRevision int
 
+	// bps is the egress rate of the endpoint
+	bps uint64
+
 	// mac is the MAC address of the endpoint
 	//
 	mac mac.MAC // Container MAC address.
@@ -1646,6 +1650,13 @@ func (e *Endpoint) RunMetadataResolver(resolveMetadata MetadataResolverCB) {
 					}
 					return annotations[annotation.ProxyVisibility], nil
 				})
+				e.UpdateBandwidthPolicy(func(ns, podName string) (bandwidthEgress string, err error) {
+					_, _, _, _, annotations, err := resolveMetadata(ns, podName)
+					if err != nil {
+						return "", err
+					}
+					return annotations[bandwidth.EgressBandwidth], nil
+				})
 				e.UpdateLabels(ctx, identityLabels, info, true)
 				close(done)
 				return nil
diff --git a/pkg/endpoint/events.go b/pkg/endpoint/events.go
index 241bf314f4..b04f60de2d 100644
--- a/pkg/endpoint/events.go
+++ b/pkg/endpoint/events.go
@@ -15,8 +15,13 @@
 package endpoint
 
 import (
+	"strconv"
+
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/eventqueue"
 	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/maps/bwmap"
+	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 
 	"github.com/sirupsen/logrus"
@@ -179,3 +184,65 @@ func (ev *EndpointPolicyVisibilityEvent) Handle(res chan interface{}) {
 	}
 	return
 }
+
+// EndpointPolicyBandwidthEvent contains all fields necessary to update
+// the Pod's bandwidth policy.
+type EndpointPolicyBandwidthEvent struct {
+	ep     *Endpoint
+	annoCB AnnotationsResolverCB
+}
+
+// Handle handles the policy bandwidth update.
+func (ev *EndpointPolicyBandwidthEvent) Handle(res chan interface{}) {
+	var bps uint64
+
+	e := ev.ep
+	if err := e.lockAlive(); err != nil {
+		// If the endpoint is being deleted, we don't need to
+		// update its bandwidth policy.
+		res <- &EndpointRegenerationResult{
+			err: nil,
+		}
+		return
+	}
+	defer func() {
+		e.unlock()
+	}()
+
+	bandwidthEgress, err := ev.annoCB(e.K8sNamespace, e.K8sPodName)
+	if err != nil || !option.Config.EnableBandwidthManager {
+		res <- &EndpointRegenerationResult{
+			err: err,
+		}
+		return
+	}
+	if bandwidthEgress != "" {
+		bps, err = bandwidth.GetBytesPerSec(bandwidthEgress)
+		if err == nil {
+			err = bwmap.Update(e.ID, bps)
+		}
+	} else {
+		err = bwmap.Delete(e.ID)
+	}
+	if err != nil {
+		res <- &EndpointRegenerationResult{
+			err: err,
+		}
+		return
+	}
+
+	bpsOld := "inf"
+	bpsNew := "inf"
+	if e.bps != 0 {
+		bpsOld = strconv.FormatUint(e.bps, 10)
+	}
+	if bps != 0 {
+		bpsNew = strconv.FormatUint(bps, 10)
+	}
+	e.getLogger().Debugf("Updating %s from %s to %s bytes/sec", bandwidth.EgressBandwidth,
+		bpsOld, bpsNew)
+	e.bps = bps
+	res <- &EndpointRegenerationResult{
+		err: nil,
+	}
+}
diff --git a/pkg/endpoint/metrics.go b/pkg/endpoint/metrics.go
index c1cc0a486f..f84f830076 100644
--- a/pkg/endpoint/metrics.go
+++ b/pkg/endpoint/metrics.go
@@ -75,10 +75,12 @@ func (s *regenerationStatistics) SendMetrics() {
 	if !s.success {
 		// Endpoint regeneration failed, increase on failed metrics
 		metrics.EndpointRegenerationCount.WithLabelValues(metrics.LabelValueOutcomeFail).Inc()
+		metrics.EndpointRegenerationTotal.WithLabelValues(metrics.LabelValueOutcomeFail).Inc()
 		return
 	}
 
 	metrics.EndpointRegenerationCount.WithLabelValues(metrics.LabelValueOutcomeSuccess).Inc()
+	metrics.EndpointRegenerationTotal.WithLabelValues(metrics.LabelValueOutcomeSuccess).Inc()
 
 	sendMetrics(s, metrics.EndpointRegenerationTimeStats)
 }
diff --git a/pkg/endpoint/policy.go b/pkg/endpoint/policy.go
index a8e5944228..d1e5f98ce9 100644
--- a/pkg/endpoint/policy.go
+++ b/pkg/endpoint/policy.go
@@ -689,20 +689,10 @@ func (e *Endpoint) startRegenerationFailureHandler() {
 }
 
 func (e *Endpoint) notifyEndpointRegeneration(err error) {
-	repr, reprerr := monitorAPI.EndpointRegenRepr(e, err)
+	reprerr := e.owner.SendNotification(monitorAPI.EndpointRegenMessage(e, err))
 	if reprerr != nil {
 		e.getLogger().WithError(reprerr).Warn("Notifying monitor about endpoint regeneration failed")
 	}
-
-	if err != nil {
-		if reprerr == nil && !option.Config.DryMode {
-			e.owner.SendNotification(monitorAPI.AgentNotifyEndpointRegenerateFail, repr)
-		}
-	} else {
-		if reprerr == nil && !option.Config.DryMode {
-			e.owner.SendNotification(monitorAPI.AgentNotifyEndpointRegenerateSuccess, repr)
-		}
-	}
 }
 
 // FormatGlobalEndpointID returns the global ID of endpoint in the format
@@ -852,6 +842,20 @@ func (e *Endpoint) UpdateVisibilityPolicy(annoCB AnnotationsResolverCB) {
 	<-ch
 }
 
+// UpdateBandwidthPolicy updates the egress bandwidth of this endpoint to
+// progagate the throttle rate to the BPF data path.
+func (e *Endpoint) UpdateBandwidthPolicy(annoCB AnnotationsResolverCB) {
+	ch, err := e.eventQueue.Enqueue(eventqueue.NewEvent(&EndpointPolicyBandwidthEvent{
+		ep:     e,
+		annoCB: annoCB,
+	}))
+	if err != nil {
+		e.getLogger().WithError(err).Error("Unable to enqueue endpoint policy bandwidth event")
+		return
+	}
+	<-ch
+}
+
 // GetRealizedPolicyRuleLabelsForKey returns the list of policy rule labels
 // which match a given flow key (in host byte-order). The returned
 // LabelArrayList must not be modified. This function is exported to be
diff --git a/pkg/endpoint/regeneration/owner.go b/pkg/endpoint/regeneration/owner.go
index 5d7dbcac41..2c37e31489 100644
--- a/pkg/endpoint/regeneration/owner.go
+++ b/pkg/endpoint/regeneration/owner.go
@@ -42,7 +42,7 @@ type Owner interface {
 	GetCIDRPrefixLengths() (s6, s4 []int)
 
 	// SendNotification is called to emit an agent notification
-	SendNotification(typ monitorAPI.AgentNotification, text string) error
+	SendNotification(msg monitorAPI.AgentNotifyMessage) error
 
 	// Datapath returns a reference to the datapath implementation.
 	Datapath() datapath.Datapath
diff --git a/pkg/endpointmanager/manager.go b/pkg/endpointmanager/manager.go
index 8c93175246..604ca30b05 100644
--- a/pkg/endpointmanager/manager.go
+++ b/pkg/endpointmanager/manager.go
@@ -150,18 +150,29 @@ func (mgr *EndpointManager) InitMetrics() {
 	if option.Config.DryMode {
 		return
 	}
-	metricsOnce.Do(func() { // EndpointCount is a function used to collect this metric. We cannot
-		// increment/decrement a gauge since we invoke Remove gratuitiously and that
+	metricsOnce.Do(func() { // Endpoint is a function used to collect this metric. We cannot
+		// increment/decrement a gauge since we invoke Remove gratuitously and that
 		// would result in negative counts.
 		// It must be thread-safe.
+
+		//TODO(sayboras): Remove deprecated metric in 1.10
 		metrics.EndpointCount = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
 			Namespace: metrics.Namespace,
 			Name:      "endpoint_count",
-			Help:      "Number of endpoints managed by this agent",
+			Help:      "Number of endpoints managed by this agent (deprecated, use endpoint instead)",
 		},
 			func() float64 { return float64(len(mgr.GetEndpoints())) },
 		)
 		metrics.MustRegister(metrics.EndpointCount)
+
+		metrics.Endpoint = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
+			Namespace: metrics.Namespace,
+			Name:      "endpoint",
+			Help:      "Number of endpoints managed by this agent",
+		},
+			func() float64 { return float64(len(mgr.GetEndpoints())) },
+		)
+		metrics.MustRegister(metrics.Endpoint)
 	})
 }
 
@@ -475,12 +486,8 @@ func (mgr *EndpointManager) AddEndpoint(owner regeneration.Owner, ep *endpoint.E
 	if err != nil {
 		return err
 	}
+	owner.SendNotification(monitorAPI.EndpointCreateMessage(ep))
 
-	repr, err := monitorAPI.EndpointCreateRepr(ep)
-	// Ignore endpoint creation if EndpointCreateRepr != nil
-	if err == nil {
-		owner.SendNotification(monitorAPI.AgentNotifyEndpointCreated, repr)
-	}
 	return nil
 }
 
diff --git a/pkg/identity/cache/allocator.go b/pkg/identity/cache/allocator.go
index ec6f26da96..674a5ca83e 100644
--- a/pkg/identity/cache/allocator.go
+++ b/pkg/identity/cache/allocator.go
@@ -308,6 +308,7 @@ func (m *CachingIdentityAllocator) AllocateIdentity(ctx context.Context, lbls la
 	defer func() {
 		if err == nil {
 			if allocated || isNewLocally {
+				metrics.Identity.Inc()
 				metrics.IdentityCount.Inc()
 			}
 
@@ -377,6 +378,7 @@ func (m *CachingIdentityAllocator) AllocateIdentity(ctx context.Context, lbls la
 func (m *CachingIdentityAllocator) Release(ctx context.Context, id *identity.Identity) (released bool, err error) {
 	defer func() {
 		if released {
+			metrics.Identity.Dec()
 			metrics.IdentityCount.Dec()
 		}
 	}()
diff --git a/pkg/ipam/metrics/metrics.go b/pkg/ipam/metrics/metrics.go
index 65bd56db5b..4346b127c8 100644
--- a/pkg/ipam/metrics/metrics.go
+++ b/pkg/ipam/metrics/metrics.go
@@ -58,21 +58,21 @@ func NewPrometheusMetrics(namespace string, registry *prometheus.Registry) *prom
 		Subsystem: ipamSubsystem,
 		Name:      "allocation_ops",
 		Help:      "Number of IP allocation operations",
-	}, []string{"subnetId"})
+	}, []string{"subnetId", "subnet_id"}) //TODO(sayboras): Remove deprecated tag subnetId in 1.10
 
 	m.ReleaseIpOps = prometheus.NewCounterVec(prometheus.CounterOpts{
 		Namespace: namespace,
 		Subsystem: ipamSubsystem,
 		Name:      "release_ops",
 		Help:      "Number of IP release operations",
-	}, []string{"subnetId"})
+	}, []string{"subnetId", "subnet_id"}) //TODO(sayboras): Remove deprecated tag subnetId in 1.10
 
 	m.AllocateInterfaceOps = prometheus.NewCounterVec(prometheus.CounterOpts{
 		Namespace: namespace,
 		Subsystem: ipamSubsystem,
 		Name:      "interface_creation_ops",
 		Help:      "Number of interfaces allocated",
-	}, []string{"subnetId", "status"})
+	}, []string{"subnetId", "subnet_id", "status"}) //TODO(sayboras): Remove deprecated tag subnetId in 1.10
 
 	m.AvailableInterfaces = prometheus.NewGauge(prometheus.GaugeOpts{
 		Namespace: namespace,
@@ -86,7 +86,9 @@ func NewPrometheusMetrics(namespace string, registry *prometheus.Registry) *prom
 		Subsystem: ipamSubsystem,
 		Name:      "available_ips_per_subnet",
 		Help:      "Number of available IPs per subnet ID",
-	}, []string{"subnetId", "availabilityZone"})
+	},
+		//TODO(sayboras): Remove deprecated tag subnetId, availabilityZone in 1.10
+		[]string{"subnetId", "subnet_id", "availabilityZone", "availability_zone"})
 
 	m.Nodes = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 		Namespace: namespace,
@@ -136,15 +138,18 @@ func (p *prometheusMetrics) ResyncTrigger() trigger.MetricsObserver {
 }
 
 func (p *prometheusMetrics) IncAllocationAttempt(status, subnetID string) {
-	p.AllocateInterfaceOps.WithLabelValues(subnetID, status).Inc()
+	// TODO(sayboras): Remove deprecated tag subnetID in 1.10
+	p.AllocateInterfaceOps.WithLabelValues(subnetID, subnetID, status).Inc()
 }
 
 func (p *prometheusMetrics) AddIPAllocation(subnetID string, allocated int64) {
-	p.AllocateIpOps.WithLabelValues(subnetID).Add(float64(allocated))
+	// TODO(sayboras): Remove deprecated tag subnetID in 1.10
+	p.AllocateIpOps.WithLabelValues(subnetID, subnetID).Add(float64(allocated))
 }
 
 func (p *prometheusMetrics) AddIPRelease(subnetID string, released int64) {
-	p.ReleaseIpOps.WithLabelValues(subnetID).Add(float64(released))
+	// TODO(sayboras): Remove deprecated tag subnetID in 1.10
+	p.ReleaseIpOps.WithLabelValues(subnetID, subnetID).Add(float64(released))
 }
 
 func (p *prometheusMetrics) SetAllocatedIPs(typ string, allocated int) {
@@ -156,7 +161,8 @@ func (p *prometheusMetrics) SetAvailableInterfaces(available int) {
 }
 
 func (p *prometheusMetrics) SetAvailableIPsPerSubnet(subnetID string, availabilityZone string, available int) {
-	p.AvailableIPsPerSubnet.WithLabelValues(subnetID, availabilityZone).Set(float64(available))
+	// TODO(sayboras): Remove deprecated tag subnetID and availabilityZone in 1.10
+	p.AvailableIPsPerSubnet.WithLabelValues(subnetID, subnetID, availabilityZone, availabilityZone).Set(float64(available))
 }
 
 func (p *prometheusMetrics) SetNodes(label string, nodes int) {
diff --git a/pkg/k8s/watchers/pod.go b/pkg/k8s/watchers/pod.go
index 70f52695fa..a6eeb8f96c 100644
--- a/pkg/k8s/watchers/pod.go
+++ b/pkg/k8s/watchers/pod.go
@@ -25,6 +25,7 @@ import (
 	"sync"
 
 	"github.com/cilium/cilium/pkg/annotation"
+	"github.com/cilium/cilium/pkg/bandwidth"
 	"github.com/cilium/cilium/pkg/comparator"
 	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/endpoint"
@@ -224,7 +225,9 @@ func (k *K8sWatcher) updateK8sPodV1(oldK8sPod, newK8sPod *slim_corev1.Pod) error
 	// Check annotation updates.
 	oldAnno := oldK8sPod.ObjectMeta.Annotations
 	newAnno := newK8sPod.ObjectMeta.Annotations
-	annotationsChanged := !k8s.AnnotationsEqual([]string{annotation.ProxyVisibility}, oldAnno, newAnno)
+	annoChangedProxy := !k8s.AnnotationsEqual([]string{annotation.ProxyVisibility}, oldAnno, newAnno)
+	annoChangedBandwidth := !k8s.AnnotationsEqual([]string{bandwidth.EgressBandwidth}, oldAnno, newAnno)
+	annotationsChanged := annoChangedProxy || annoChangedBandwidth
 
 	// Check label updates too.
 	oldPodLabels := oldK8sPod.ObjectMeta.Labels
@@ -255,13 +258,24 @@ func (k *K8sWatcher) updateK8sPodV1(oldK8sPod, newK8sPod *slim_corev1.Pod) error
 	}
 
 	if annotationsChanged {
-		podEP.UpdateVisibilityPolicy(func(ns, podName string) (proxyVisibility string, err error) {
-			p, err := k.GetCachedPod(ns, podName)
-			if err != nil {
-				return "", nil
-			}
-			return p.ObjectMeta.Annotations[annotation.ProxyVisibility], nil
-		})
+		if annoChangedProxy {
+			podEP.UpdateVisibilityPolicy(func(ns, podName string) (proxyVisibility string, err error) {
+				p, err := k.GetCachedPod(ns, podName)
+				if err != nil {
+					return "", nil
+				}
+				return p.ObjectMeta.Annotations[annotation.ProxyVisibility], nil
+			})
+		}
+		if annoChangedBandwidth {
+			podEP.UpdateBandwidthPolicy(func(ns, podName string) (bandwidthEgress string, err error) {
+				p, err := k.GetCachedPod(ns, podName)
+				if err != nil {
+					return "", nil
+				}
+				return p.ObjectMeta.Annotations[bandwidth.EgressBandwidth], nil
+			})
+		}
 		realizePodAnnotationUpdate(podEP)
 	}
 	return nil
diff --git a/pkg/k8s/watchers/watcher.go b/pkg/k8s/watchers/watcher.go
index 555c31ec77..82ad8efa32 100644
--- a/pkg/k8s/watchers/watcher.go
+++ b/pkg/k8s/watchers/watcher.go
@@ -241,7 +241,8 @@ func (*k8sMetrics) Observe(verb string, u url.URL, latency time.Duration) {
 }
 
 func (*k8sMetrics) Increment(code string, method string, host string) {
-	metrics.KubernetesAPICalls.WithLabelValues(host, method, code).Inc()
+	metrics.KubernetesAPICalls.WithLabelValues(host, method, code).Inc() //TODO(sayboras): Remove deprecated metric in 1.10
+	metrics.KubernetesAPICallsTotal.WithLabelValues(host, method, code).Inc()
 	// The 'code' is set to '<error>' in case an error is returned from k8s
 	// more info:
 	// https://github.com/kubernetes/client-go/blob/v0.18.0-rc.1/rest/request.go#L700-L703
diff --git a/pkg/kvstore/etcd.go b/pkg/kvstore/etcd.go
index 50d5e2ff72..eee6aad6fb 100644
--- a/pkg/kvstore/etcd.go
+++ b/pkg/kvstore/etcd.go
@@ -51,9 +51,11 @@ const (
 	// EtcdBackendName is the backend name for etcd
 	EtcdBackendName = "etcd"
 
-	EtcdAddrOption       = "etcd.address"
-	isEtcdOperatorOption = "etcd.operator"
-	EtcdOptionConfig     = "etcd.config"
+	EtcdAddrOption               = "etcd.address"
+	isEtcdOperatorOption         = "etcd.operator"
+	EtcdOptionConfig             = "etcd.config"
+	etcdOptionKeepAliveHeartbeat = "etcd.keepaliveHeartbeat"
+	etcdOptionKeepAliveTimeout   = "etcd.keepaliveTimeout"
 
 	// EtcdRateLimitOption specifies maximum kv operations per second
 	EtcdRateLimitOption = "etcd.qps"
@@ -116,6 +118,20 @@ func newEtcdModule() backendModule {
 			EtcdOptionConfig: &backendOption{
 				description: "Path to etcd configuration file",
 			},
+			etcdOptionKeepAliveTimeout: &backendOption{
+				description: "Timeout after which an unanswered heartbeat triggers the connection to be closed",
+				validate: func(v string) error {
+					_, err := time.ParseDuration(v)
+					return err
+				},
+			},
+			etcdOptionKeepAliveHeartbeat: &backendOption{
+				description: "Heartbeat interval to keep gRPC connection alive",
+				validate: func(v string) error {
+					_, err := time.ParseDuration(v)
+					return err
+				},
+			},
 			EtcdRateLimitOption: &backendOption{
 				description: "Rate limit in kv store operations per second",
 				validate: func(v string) error {
@@ -162,6 +178,12 @@ func shuffleEndpoints(endpoints []string) {
 	})
 }
 
+type clientOptions struct {
+	KeepAliveHeartbeat time.Duration
+	KeepAliveTimeout   time.Duration
+	RateLimit          int
+}
+
 func (e *etcdModule) newClient(ctx context.Context, opts *ExtraOptions) (BackendOperations, chan error) {
 	errChan := make(chan error, 10)
 
@@ -170,10 +192,23 @@ func (e *etcdModule) newClient(ctx context.Context, opts *ExtraOptions) (Backend
 
 	rateLimitOpt, rateLimitSet := e.opts[EtcdRateLimitOption]
 
-	rateLimit := defaults.KVstoreQPS
+	clientOptions := clientOptions{
+		KeepAliveHeartbeat: 15 * time.Second,
+		KeepAliveTimeout:   25 * time.Second,
+		RateLimit:          defaults.KVstoreQPS,
+	}
+
 	if rateLimitSet {
 		// error is discarded here because this option has validation
-		rateLimit, _ = strconv.Atoi(rateLimitOpt.value)
+		clientOptions.RateLimit, _ = strconv.Atoi(rateLimitOpt.value)
+	}
+
+	if o, ok := e.opts[etcdOptionKeepAliveTimeout]; ok && o.value != "" {
+		clientOptions.KeepAliveTimeout, _ = time.ParseDuration(o.value)
+	}
+
+	if o, ok := e.opts[etcdOptionKeepAliveHeartbeat]; ok && o.value != "" {
+		clientOptions.KeepAliveHeartbeat, _ = time.ParseDuration(o.value)
 	}
 
 	var configPath string
@@ -211,7 +246,7 @@ func (e *etcdModule) newClient(ctx context.Context, opts *ExtraOptions) (Backend
 	for {
 		// connectEtcdClient will close errChan when the connection attempt has
 		// been successful
-		backend, err := connectEtcdClient(ctx, e.config, configPath, errChan, rateLimit, opts)
+		backend, err := connectEtcdClient(ctx, e.config, configPath, errChan, clientOptions, opts)
 		switch {
 		case os.IsNotExist(err):
 			log.WithError(err).Info("Waiting for all etcd configuration files to be available")
@@ -625,7 +660,7 @@ func (e *etcdClient) renewLockSession(ctx context.Context) error {
 	return nil
 }
 
-func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath string, errChan chan error, rateLimit int, opts *ExtraOptions) (BackendOperations, error) {
+func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath string, errChan chan error, clientOptions clientOptions, opts *ExtraOptions) (BackendOperations, error) {
 	if cfgPath != "" {
 		cfg, err := newConfig(cfgPath)
 		if err != nil {
@@ -645,6 +680,11 @@ func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath strin
 	// block until DialTimeout is reached or a connection to the server
 	// is made.
 	config.DialTimeout = 0
+	// Ping the server to verify if the server connection is still valid
+	config.DialKeepAliveTime = clientOptions.KeepAliveHeartbeat
+	// Timeout if the server does not reply within 15 seconds and close the
+	// connection. Ideally it should be lower than staleLockTimeout
+	config.DialKeepAliveTimeout = clientOptions.KeepAliveTimeout
 	c, err := client.New(*config)
 	if err != nil {
 		return nil, err
@@ -691,7 +731,7 @@ func connectEtcdClient(ctx context.Context, config *client.Config, cfgPath strin
 		latestStatusSnapshot: "Waiting for initial connection to be established",
 		stopStatusChecker:    make(chan struct{}),
 		extraOptions:         opts,
-		limiter:              rate.NewLimiter(rate.Limit(rateLimit), rateLimit),
+		limiter:              rate.NewLimiter(rate.Limit(clientOptions.RateLimit), clientOptions.RateLimit),
 		statusCheckErrors:    make(chan error, 128),
 	}
 
diff --git a/pkg/logging/logfields/logfields.go b/pkg/logging/logfields/logfields.go
index fd3fad3dc7..0aaa6b2e09 100644
--- a/pkg/logging/logfields/logfields.go
+++ b/pkg/logging/logfields/logfields.go
@@ -16,6 +16,8 @@
 package logfields
 
 const (
+	// Annotations are any annotations for Pods
+	Annotations = "annotations"
 
 	// LogSubsys is the field denoting the subsystem when logging
 	LogSubsys = "subsys"
@@ -250,6 +252,12 @@ const (
 	// CiliumNetworkPolicyName is the name of a CiliumNetworkPolicy
 	CiliumNetworkPolicyName = "ciliumNetworkPolicyName"
 
+	// BPFClockSource denotes the internal clock source (ktime vs jiffies)
+	BPFClockSource = "bpfClockSource"
+
+	// BPFInsnSet denotes the instruction set version
+	BPFInsnSet = "bpfInsnSet"
+
 	// BPFMapKey is a key from a BPF map
 	BPFMapKey = "bpfMapKey"
 
diff --git a/pkg/maps/bwmap/bwmap.go b/pkg/maps/bwmap/bwmap.go
new file mode 100644
index 0000000000..a93845d4c4
--- /dev/null
+++ b/pkg/maps/bwmap/bwmap.go
@@ -0,0 +1,79 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bwmap
+
+import (
+	"fmt"
+	"time"
+	"unsafe"
+
+	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/maps/lxcmap"
+)
+
+const (
+	MapName = "cilium_throttle"
+	// Flow aggregate is per Pod, so same size as Endpoint map.
+	MapSize = lxcmap.MaxEntries
+
+	// DefaultDropHorizon represents maximum allowed departure
+	// time delta in future. Given applications can set SO_TXTIME
+	// from user space this is a limit to prevent buggy applications
+	// to fill the FQ qdisc.
+	DefaultDropHorizon = 2 * time.Second
+)
+
+type EdtId struct {
+	Id uint64 `align:"id"`
+}
+
+func (k *EdtId) GetKeyPtr() unsafe.Pointer  { return unsafe.Pointer(k) }
+func (k *EdtId) NewValue() bpf.MapValue     { return &EdtInfo{} }
+func (k *EdtId) String() string             { return fmt.Sprintf("%d", int(k.Id)) }
+func (k *EdtId) DeepCopyMapKey() bpf.MapKey { return &EdtId{k.Id} }
+
+type EdtInfo struct {
+	Bps             uint64    `align:"bps"`
+	TimeLast        uint64    `align:"t_last"`
+	TimeHorizonDrop uint64    `align:"t_horizon_drop"`
+	Pad             [4]uint64 `align:"pad"`
+}
+
+func (v *EdtInfo) GetValuePtr() unsafe.Pointer { return unsafe.Pointer(v) }
+func (v *EdtInfo) String() string              { return fmt.Sprintf("%d", int(v.Bps)) }
+func (v *EdtInfo) DeepCopyMapValue() bpf.MapValue {
+	return &EdtInfo{v.Bps, v.TimeLast, v.TimeHorizonDrop, v.Pad}
+}
+
+var ThrottleMap = bpf.NewMap(
+	MapName,
+	bpf.MapTypeHash,
+	&EdtId{}, int(unsafe.Sizeof(EdtId{})),
+	&EdtInfo{}, int(unsafe.Sizeof(EdtInfo{})),
+	MapSize,
+	bpf.BPF_F_NO_PREALLOC, 0,
+	bpf.ConvertKeyValue,
+).WithCache()
+
+func Update(Id uint16, Bps uint64) error {
+	return ThrottleMap.Update(
+		&EdtId{Id: uint64(Id)},
+		&EdtInfo{Bps: Bps, TimeHorizonDrop: uint64(DefaultDropHorizon)})
+}
+
+func Delete(Id uint16) error {
+	return ThrottleMap.Delete(
+		&EdtId{Id: uint64(Id)})
+}
diff --git a/pkg/maps/bwmap/doc.go b/pkg/maps/bwmap/doc.go
new file mode 100644
index 0000000000..ba343845ac
--- /dev/null
+++ b/pkg/maps/bwmap/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package bwmap represents the BPF map used to enforce Pod bandwidth
+// limitations via EDT (Earliest Departure Time) + BPF.
+// +groupName=maps
+package bwmap
diff --git a/pkg/maps/metricsmap/metricsmap.go b/pkg/maps/metricsmap/metricsmap.go
index caee8787ea..88be87569b 100644
--- a/pkg/maps/metricsmap/metricsmap.go
+++ b/pkg/maps/metricsmap/metricsmap.go
@@ -193,7 +193,7 @@ func updateMetric(getCounter func() (prometheus.Counter, error), newValue float6
 
 	oldValue := metrics.GetCounterValue(counter)
 	if newValue > oldValue {
-		counter.Add((newValue - oldValue))
+		counter.Add(newValue - oldValue)
 	}
 }
 
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index cbb2803112..fdc663848c 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -165,8 +165,13 @@ const (
 	// LabelOperation is the label for BPF maps operations
 	LabelOperation = "operation"
 
+	// TODO(sayboras): Remove deprecated metric in 1.10
+	// LabelMapNameDeprecated is the label for the BPF map name
+	// Deprecated: in favor of LabelMapName
+	LabelMapNameDeprecated = "mapName"
+
 	// LabelMapName is the label for the BPF map name
-	LabelMapName = "mapName"
+	LabelMapName = "map_name"
 
 	// LabelVersion is the label for the version number
 	LabelVersion = "version"
@@ -181,14 +186,26 @@ var (
 
 	// Endpoint
 
+	// Endpoint is a function used to collect this metric.
+	// It must be thread-safe.
+	Endpoint prometheus.GaugeFunc
+
+	// TODO(sayboras): Remove deprecated metric in 1.10
 	// EndpointCount is a function used to collect this metric.
 	// It must be thread-safe.
+	// Deprecated: in favor of Endpoint
 	EndpointCount prometheus.GaugeFunc
 
+	// TODO(sayboras): Remove deprecated metric in 1.10
 	// EndpointRegenerationCount is a count of the number of times any endpoint
 	// has been regenerated and success/fail outcome
+	// Deprecated: in favor of EndpointRegenerationTotal
 	EndpointRegenerationCount = NoOpCounterVec
 
+	// EndpointRegenerationTotal is a count of the number of times any endpoint
+	// has been regenerated and success/fail outcome
+	EndpointRegenerationTotal = NoOpCounterVec
+
 	// EndpointStateCount is the total count of the endpoints in various states.
 	EndpointStateCount = NoOpGaugeVec
 
@@ -198,7 +215,12 @@ var (
 
 	// Policy
 
+	// Policy is the number of policies loaded into the agent
+	Policy = NoOpGauge
+
+	// TODO(sayboras): Remove deprecated metric in 1.10
 	// PolicyCount is the number of policies loaded into the agent
+	// Deprecated: in favor of Policy
 	PolicyCount = NoOpGauge
 
 	// PolicyRegenerationCount is the total number of successful policy
@@ -211,9 +233,14 @@ var (
 	// PolicyRevision is the current policy revision number for this agent
 	PolicyRevision = NoOpGauge
 
+	// TODO(sayboras): Remove deprecated metric in 1.10
 	// PolicyImportErrors is a count of failed policy imports
+	// Deprecated: in favor of PolicyImportErrorsTotal
 	PolicyImportErrors = NoOpCounter
 
+	// PolicyImportErrorsTotal is a count of failed policy imports
+	PolicyImportErrorsTotal = NoOpCounter
+
 	// PolicyEndpointStatus is the number of endpoints with policy labeled by enforcement type
 	PolicyEndpointStatus = NoOpGaugeVec
 
@@ -226,7 +253,12 @@ var (
 
 	// Identity
 
+	// Identity is the number of identities currently in use on the node
+	Identity = NoOpGauge
+
+	// TODO(sayboras): Remove deprecated metric in 1.10
 	// IdentityCount is the number of identities currently in use on the node
+	// Deprecated: in favor of Identity
 	IdentityCount = NoOpGauge
 
 	// Events
@@ -349,10 +381,16 @@ var (
 	// to the kube-apiserver
 	KubernetesAPIInteractions = NoOpObserverVec
 
+	// TODO(sayboras): Remove deprecated metric in 1.10
 	// KubernetesAPICalls is the counter for all API calls made to
 	// kube-apiserver.
+	// Deprecated: Use KubernetesAPICallsTotal instead
 	KubernetesAPICalls = NoOpCounterVec
 
+	// KubernetesAPICallsTotal is the counter for all API calls made to
+	// kube-apiserver.
+	KubernetesAPICallsTotal = NoOpCounterVec
+
 	// KubernetesCNPStatusCompletion is the number of seconds it takes to
 	// complete a CNP status update
 	KubernetesCNPStatusCompletion = NoOpObserverVec
@@ -461,17 +499,21 @@ type Configuration struct {
 func DefaultMetrics() map[string]struct{} {
 	return map[string]struct{}{
 		Namespace + "_" + SubsystemAgent + "_api_process_time_seconds":               {},
-		Namespace + "_endpoint_regenerations":                                        {},
+		Namespace + "_endpoint_regenerations":                                        {}, //TODO(sayboras): Remove deprecated metric in 1.10
+		Namespace + "_endpoint_regenerations_total":                                  {},
 		Namespace + "_endpoint_state":                                                {},
 		Namespace + "_endpoint_regeneration_time_stats_seconds":                      {},
-		Namespace + "_policy_count":                                                  {},
+		Namespace + "_policy":                                                        {},
+		Namespace + "_policy_count":                                                  {}, //TODO(sayboras): Remove deprecated metric in 1.10
 		Namespace + "_policy_regeneration_total":                                     {},
 		Namespace + "_policy_regeneration_time_stats_seconds":                        {},
 		Namespace + "_policy_max_revision":                                           {},
-		Namespace + "_policy_import_errors":                                          {},
+		Namespace + "_policy_import_errors":                                          {}, //TODO(sayboras): Remove deprecated metric in 1.10
+		Namespace + "_policy_import_errors_total":                                    {},
 		Namespace + "_policy_endpoint_enforcement_status":                            {},
 		Namespace + "_policy_implementation_delay":                                   {},
-		Namespace + "_identity_count":                                                {},
+		Namespace + "_identity":                                                      {},
+		Namespace + "_identity_count":                                                {}, //TODO(sayboras): Remove deprecated metric in 1.10
 		Namespace + "_event_ts":                                                      {},
 		Namespace + "_proxy_redirects":                                               {},
 		Namespace + "_policy_l7_total":                                               {},
@@ -498,7 +540,8 @@ func DefaultMetrics() map[string]struct{} {
 		Namespace + "_kubernetes_events_total":                                       {},
 		Namespace + "_kubernetes_events_received_total":                              {},
 		Namespace + "_" + SubsystemK8sClient + "_api_latency_time_seconds":           {},
-		Namespace + "_" + SubsystemK8sClient + "_api_calls_counter":                  {},
+		Namespace + "_" + SubsystemK8sClient + "_api_calls_counter":                  {}, //TODO(sayboras): Remove deprecated metric in 1.10
+		Namespace + "_" + SubsystemK8sClient + "_api_calls_total":                    {},
 		Namespace + "_" + SubsystemK8s + "_cnp_status_completion_seconds":            {},
 		Namespace + "_ipam_events_total":                                             {},
 		Namespace + "_" + SubsystemKVStore + "_operations_duration_seconds":          {},
@@ -538,12 +581,23 @@ func CreateConfiguration(metricsEnabled []string) (Configuration, []prometheus.C
 			EndpointRegenerationCount = prometheus.NewCounterVec(prometheus.CounterOpts{
 				Namespace: Namespace,
 				Name:      "endpoint_regenerations",
-				Help:      "Count of all endpoint regenerations that have completed, tagged by outcome",
+				Help: "Count of all endpoint regenerations that have completed, tagged by outcome" +
+					"(deprecated, use endpoint_regenerations_total instead)",
 			}, []string{"outcome"})
 
 			collectors = append(collectors, EndpointRegenerationCount)
 			c.EndpointRegenerationCountEnabled = true
 
+		case Namespace + "_endpoint_regenerations_total":
+			EndpointRegenerationTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
+				Namespace: Namespace,
+				Name:      "endpoint_regenerations_total",
+				Help:      "Count of all endpoint regenerations that have completed, tagged by outcome",
+			}, []string{"outcome"})
+
+			collectors = append(collectors, EndpointRegenerationTotal)
+			c.EndpointRegenerationCountEnabled = true
+
 		case Namespace + "_endpoint_state":
 			EndpointStateCount = prometheus.NewGaugeVec(
 				prometheus.GaugeOpts{
@@ -567,11 +621,21 @@ func CreateConfiguration(metricsEnabled []string) (Configuration, []prometheus.C
 			collectors = append(collectors, EndpointRegenerationTimeStats)
 			c.EndpointRegenerationTimeStatsEnabled = true
 
+		case Namespace + "_policy":
+			Policy = prometheus.NewGauge(prometheus.GaugeOpts{
+				Namespace: Namespace,
+				Name:      "policy",
+				Help:      "Number of policies currently loaded",
+			})
+
+			collectors = append(collectors, Policy)
+			c.PolicyCountEnabled = true
+
 		case Namespace + "_policy_count":
 			PolicyCount = prometheus.NewGauge(prometheus.GaugeOpts{
 				Namespace: Namespace,
 				Name:      "policy_count",
-				Help:      "Number of policies currently loaded",
+				Help:      "Number of policies currently loaded (deprecated, use policy instead)",
 			})
 
 			collectors = append(collectors, PolicyCount)
@@ -611,12 +675,23 @@ func CreateConfiguration(metricsEnabled []string) (Configuration, []prometheus.C
 			PolicyImportErrors = prometheus.NewCounter(prometheus.CounterOpts{
 				Namespace: Namespace,
 				Name:      "policy_import_errors",
-				Help:      "Number of times a policy import has failed",
+				Help: "Number of times a policy import has failed" +
+					"(deprecated, use policy_import_errors_total instead)",
 			})
 
 			collectors = append(collectors, PolicyImportErrors)
 			c.PolicyImportErrorsEnabled = true
 
+		case Namespace + "_policy_import_errors_total":
+			PolicyImportErrorsTotal = prometheus.NewCounter(prometheus.CounterOpts{
+				Namespace: Namespace,
+				Name:      "policy_import_errors_total",
+				Help:      "Number of times a policy import has failed",
+			})
+
+			collectors = append(collectors, PolicyImportErrorsTotal)
+			c.PolicyImportErrorsEnabled = true
+
 		case Namespace + "_policy_endpoint_enforcement_status":
 			PolicyEndpointStatus = prometheus.NewGaugeVec(prometheus.GaugeOpts{
 				Namespace: Namespace,
@@ -637,11 +712,21 @@ func CreateConfiguration(metricsEnabled []string) (Configuration, []prometheus.C
 			collectors = append(collectors, PolicyImplementationDelay)
 			c.PolicyImplementationDelayEnabled = true
 
+		case Namespace + "_identity":
+			Identity = prometheus.NewGauge(prometheus.GaugeOpts{
+				Namespace: Namespace,
+				Name:      "identity",
+				Help:      "Number of identities currently allocated",
+			})
+
+			collectors = append(collectors, Identity)
+			c.IdentityCountEnabled = true
+
 		case Namespace + "_identity_count":
 			IdentityCount = prometheus.NewGauge(prometheus.GaugeOpts{
 				Namespace: Namespace,
 				Name:      "identity_count",
-				Help:      "Number of identities currently allocated",
+				Help:      "Number of identities currently allocated (deprecated, use identity instead)",
 			})
 
 			collectors = append(collectors, IdentityCount)
@@ -948,12 +1033,24 @@ func CreateConfiguration(metricsEnabled []string) (Configuration, []prometheus.C
 				Namespace: Namespace,
 				Subsystem: SubsystemK8sClient,
 				Name:      "api_calls_counter",
-				Help:      "Number of API calls made to kube-apiserver labeled by host, method and return code.",
+				Help: "Number of API calls made to kube-apiserver labeled by host, method and return code." +
+					"(deprecated, use api_calls_total instead)",
 			}, []string{"host", LabelMethod, LabelAPIReturnCode})
 
 			collectors = append(collectors, KubernetesAPICalls)
 			c.KubernetesAPICallsEnabled = true
 
+		case Namespace + "_" + SubsystemK8sClient + "_api_calls_total":
+			KubernetesAPICallsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
+				Namespace: Namespace,
+				Subsystem: SubsystemK8sClient,
+				Name:      "api_calls_total",
+				Help:      "Number of API calls made to kube-apiserver labeled by host, method and return code.",
+			}, []string{"host", LabelMethod, LabelAPIReturnCode})
+
+			collectors = append(collectors, KubernetesAPICallsTotal)
+			c.KubernetesAPICallsEnabled = true
+
 		case Namespace + "_" + SubsystemK8s + "_cnp_status_completion_seconds":
 			KubernetesCNPStatusCompletion = prometheus.NewHistogramVec(prometheus.HistogramOpts{
 				Namespace: Namespace,
@@ -1036,7 +1133,7 @@ func CreateConfiguration(metricsEnabled []string) (Configuration, []prometheus.C
 				Subsystem: SubsystemBPF,
 				Name:      "map_ops_total",
 				Help:      "Total operations on map, tagged by map name",
-			}, []string{LabelMapName, LabelOperation, LabelOutcome})
+			}, []string{LabelMapName, LabelMapNameDeprecated, LabelOperation, LabelOutcome})
 
 			collectors = append(collectors, BPFMapOps)
 			c.BPFMapOps = true
diff --git a/pkg/monitor/agent/agent.go b/pkg/monitor/agent/agent.go
index 42f040e51a..28952f24a2 100644
--- a/pkg/monitor/agent/agent.go
+++ b/pkg/monitor/agent/agent.go
@@ -29,6 +29,7 @@ import (
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/monitor/agent/consumer"
 	"github.com/cilium/cilium/pkg/monitor/agent/listener"
+	"github.com/cilium/cilium/pkg/monitor/api"
 	"github.com/cilium/cilium/pkg/monitor/payload"
 	"github.com/cilium/ebpf"
 	"github.com/cilium/ebpf/perf"
@@ -117,6 +118,19 @@ func (a *Agent) SendEvent(typ int, event interface{}) error {
 
 	a.notifyAgentEvent(typ, event)
 
+	// marshal notifications into JSON format for legacy listeners
+	if typ == api.MessageTypeAgent {
+		msg, ok := event.(api.AgentNotifyMessage)
+		if !ok {
+			return errors.New("unexpected event type for MessageTypeAgent")
+		}
+		var err error
+		event, err = msg.ToJSON()
+		if err != nil {
+			return fmt.Errorf("unable to JSON encode agent notification: %w", err)
+		}
+	}
+
 	var buf bytes.Buffer
 	if err := buf.WriteByte(byte(typ)); err != nil {
 		return fmt.Errorf("unable to initialize buffer: %w", err)
diff --git a/pkg/monitor/api/drop.go b/pkg/monitor/api/drop.go
index dcc0b59565..8686c5473b 100644
--- a/pkg/monitor/api/drop.go
+++ b/pkg/monitor/api/drop.go
@@ -64,6 +64,7 @@ var errors = map[uint8]string{
 	158: "Service backend not found",
 	160: "No tunnel/encapsulation endpoint (datapath BUG!)",
 	161: "Failed to insert into proxymap", // Unused
+	162: "Reached EDT rate-limiting drop horizon",
 	163: "Unknown connection tracking state",
 	164: "Local host is unreachable",
 	165: "No configuration available to perform policy decision", // Unused
diff --git a/pkg/monitor/api/types.go b/pkg/monitor/api/types.go
index e047101189..c9882e2504 100644
--- a/pkg/monitor/api/types.go
+++ b/pkg/monitor/api/types.go
@@ -170,12 +170,33 @@ func TraceObservationPoint(obsPoint uint8) string {
 	return fmt.Sprintf("%d", obsPoint)
 }
 
-// AgentNotify is a notification from the agent
+// AgentNotify is a notification from the agent. The notification is stored
+// in its JSON-encoded representation
 type AgentNotify struct {
 	Type AgentNotification
 	Text string
 }
 
+// AgentNotify is a notification from the agent. It is similar to AgentNotify,
+// but the notification is an unencoded struct. See the *Message constructors
+// in this package for possible values.
+type AgentNotifyMessage struct {
+	Type         AgentNotification
+	Notification interface{}
+}
+
+// ToJSON encodes a AgentNotifyMessage to its JSON-based AgentNotify representation
+func (m *AgentNotifyMessage) ToJSON() (AgentNotify, error) {
+	repr, err := json.Marshal(m.Notification)
+	if err != nil {
+		return AgentNotify{}, err
+	}
+	return AgentNotify{
+		Type: m.Type,
+		Text: string(repr),
+	}, nil
+}
+
 // AgentNotification specifies the type of agent notification
 type AgentNotification uint32
 
@@ -240,29 +261,32 @@ type PolicyUpdateNotification struct {
 	RuleCount int      `json:"rule_count"`
 }
 
-// PolicyUpdateRepr returns string representation of monitor notification
-func PolicyUpdateRepr(numRules int, labels []string, revision uint64) (string, error) {
+// PolicyUpdateMessage constructs an agent notification message for policy updates
+func PolicyUpdateMessage(numRules int, labels []string, revision uint64) AgentNotifyMessage {
 	notification := PolicyUpdateNotification{
 		Labels:    labels,
 		Revision:  revision,
 		RuleCount: numRules,
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyPolicyUpdated,
+		Notification: notification,
+	}
 }
 
-// PolicyDeleteRepr returns string representation of monitor notification
-func PolicyDeleteRepr(deleted int, labels []string, revision uint64) (string, error) {
+// PolicyDeleteMessage constructs an agent notification message for policy deletion
+func PolicyDeleteMessage(deleted int, labels []string, revision uint64) AgentNotifyMessage {
 	notification := PolicyUpdateNotification{
 		Labels:    labels,
 		Revision:  revision,
 		RuleCount: deleted,
 	}
-	repr, err := json.Marshal(notification)
 
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyPolicyDeleted,
+		Notification: notification,
+	}
 }
 
 // EndpointRegenNotification structures regeneration notification
@@ -272,20 +296,23 @@ type EndpointRegenNotification struct {
 	Error  string   `json:"error,omitempty"`
 }
 
-// EndpointRegenRepr returns string representation of monitor notification
-func EndpointRegenRepr(e notifications.RegenNotificationInfo, err error) (string, error) {
+// EndpointRegenMessage constructs an agent notification message for endpoint regeneration
+func EndpointRegenMessage(e notifications.RegenNotificationInfo, err error) AgentNotifyMessage {
 	notification := EndpointRegenNotification{
 		ID:     e.GetID(),
 		Labels: e.GetOpLabels(),
 	}
 
+	typ := AgentNotifyEndpointRegenerateSuccess
 	if err != nil {
 		notification.Error = err.Error()
+		typ = AgentNotifyEndpointRegenerateFail
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         typ,
+		Notification: notification,
+	}
 }
 
 // EndpointCreateNotification structures the endpoint create notification
@@ -295,8 +322,8 @@ type EndpointCreateNotification struct {
 	Namespace string `json:"namespace,omitempty"`
 }
 
-// EndpointCreateRepr returns string representation of monitor notification
-func EndpointCreateRepr(e notifications.RegenNotificationInfo) (string, error) {
+// EndpointCreateMessage constructs an agent notification message for endpoint creation
+func EndpointCreateMessage(e notifications.RegenNotificationInfo) AgentNotifyMessage {
 	notification := EndpointCreateNotification{
 		EndpointRegenNotification: EndpointRegenNotification{
 			ID:     e.GetID(),
@@ -306,9 +333,10 @@ func EndpointCreateRepr(e notifications.RegenNotificationInfo) (string, error) {
 		Namespace: e.GetK8sNamespace(),
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyEndpointCreated,
+		Notification: notification,
+	}
 }
 
 // EndpointDeleteNotification structures the an endpoint delete notification
@@ -318,8 +346,8 @@ type EndpointDeleteNotification struct {
 	Namespace string `json:"namespace,omitempty"`
 }
 
-// EndpointDeleteRepr returns string representation of monitor notification
-func EndpointDeleteRepr(e notifications.RegenNotificationInfo) (string, error) {
+// EndpointDeleteMessage constructs an agent notification message for endpoint deletion
+func EndpointDeleteMessage(e notifications.RegenNotificationInfo) AgentNotifyMessage {
 	notification := EndpointDeleteNotification{
 		EndpointRegenNotification: EndpointRegenNotification{
 			ID:     e.GetID(),
@@ -329,9 +357,10 @@ func EndpointDeleteRepr(e notifications.RegenNotificationInfo) (string, error) {
 		Namespace: e.GetK8sNamespace(),
 	}
 
-	repr, err := json.Marshal(notification)
-
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyEndpointDeleted,
+		Notification: notification,
+	}
 }
 
 // IPCacheNotification structures ipcache change notifications
@@ -348,9 +377,9 @@ type IPCacheNotification struct {
 	PodName    string `json:"pod-name,omitempty"`
 }
 
-// IPCacheNotificationRepr returns string representation of monitor notification
-func IPCacheNotificationRepr(cidr string, id uint32, oldID *uint32, hostIP net.IP, oldHostIP net.IP,
-	encryptKey uint8, namespace, podName string) (string, error) {
+// IPCacheUpsertedMessage constructs an agent notification message for ipcache upsertions
+func IPCacheUpsertedMessage(cidr string, id uint32, oldID *uint32, hostIP net.IP, oldHostIP net.IP,
+	encryptKey uint8, namespace, podName string) AgentNotifyMessage {
 	notification := IPCacheNotification{
 		CIDR:        cidr,
 		Identity:    id,
@@ -362,9 +391,30 @@ func IPCacheNotificationRepr(cidr string, id uint32, oldID *uint32, hostIP net.I
 		PodName:     podName,
 	}
 
-	repr, err := json.Marshal(notification)
+	return AgentNotifyMessage{
+		Type:         AgentNotifyIPCacheUpserted,
+		Notification: notification,
+	}
+}
+
+// IPCacheDeletedMessage constructs an agent notification message for ipcache deletions
+func IPCacheDeletedMessage(cidr string, id uint32, oldID *uint32, hostIP net.IP, oldHostIP net.IP,
+	encryptKey uint8, namespace, podName string) AgentNotifyMessage {
+	notification := IPCacheNotification{
+		CIDR:        cidr,
+		Identity:    id,
+		OldIdentity: oldID,
+		HostIP:      hostIP,
+		OldHostIP:   oldHostIP,
+		EncryptKey:  encryptKey,
+		Namespace:   namespace,
+		PodName:     podName,
+	}
 
-	return string(repr), err
+	return AgentNotifyMessage{
+		Type:         AgentNotifyIPCacheDeleted,
+		Notification: notification,
+	}
 }
 
 // TimeNotification structures agent start notification
@@ -372,13 +422,16 @@ type TimeNotification struct {
 	Time string `json:"time"`
 }
 
-// TimeRepr returns string representation of monitor notification
-func TimeRepr(t time.Time) (string, error) {
+// AgentStartMessage constructs an agent notification message when the agent starts
+func StartMessage(t time.Time) AgentNotifyMessage {
 	notification := TimeNotification{
 		Time: t.String(),
 	}
-	repr, err := json.Marshal(notification)
-	return string(repr), err
+
+	return AgentNotifyMessage{
+		Type:         AgentNotifyStart,
+		Notification: notification,
+	}
 }
 
 // ServiceUpsertNotificationAddr is part of ServiceUpsertNotification
@@ -401,13 +454,13 @@ type ServiceUpsertNotification struct {
 	Namespace string `json:"namespace,,omitempty"`
 }
 
-// ServiceUpsertRepr returns string representation of monitor notification
-func ServiceUpsertRepr(
+// ServiceUpsertMessage constructs an agent notification message for service upserts
+func ServiceUpsertMessage(
 	id uint32,
 	frontend ServiceUpsertNotificationAddr,
 	backends []ServiceUpsertNotificationAddr,
 	svcType, svcTrafficPolicy, svcName, svcNamespace string,
-) (string, error) {
+) AgentNotifyMessage {
 	notification := ServiceUpsertNotification{
 		ID:            id,
 		Frontend:      frontend,
@@ -417,8 +470,11 @@ func ServiceUpsertRepr(
 		Name:          svcName,
 		Namespace:     svcNamespace,
 	}
-	repr, err := json.Marshal(notification)
-	return string(repr), err
+
+	return AgentNotifyMessage{
+		Type:         AgentNotifyServiceUpserted,
+		Notification: notification,
+	}
 }
 
 // ServiceDeleteNotification structures service delete notifications
@@ -426,15 +482,16 @@ type ServiceDeleteNotification struct {
 	ID uint32 `json:"id"`
 }
 
-// ServiceDeleteRepr returns string representation of monitor notification
-func ServiceDeleteRepr(
-	id uint32,
-) (string, error) {
+// ServiceDeleteMessage constructs an agent notification message for service deletions
+func ServiceDeleteMessage(id uint32) AgentNotifyMessage {
 	notification := ServiceDeleteNotification{
 		ID: id,
 	}
-	repr, err := json.Marshal(notification)
-	return string(repr), err
+
+	return AgentNotifyMessage{
+		Type:         AgentNotifyServiceDeleted,
+		Notification: notification,
+	}
 }
 
 const (
diff --git a/pkg/node/manager/manager.go b/pkg/node/manager/manager.go
index 658683d9a3..6963ca7f30 100644
--- a/pkg/node/manager/manager.go
+++ b/pkg/node/manager/manager.go
@@ -62,12 +62,12 @@ type Configuration interface {
 	NodeEncryptionEnabled() bool
 }
 
-// Notifier is the interaface the wraps Subscribe and Unsubscribe. An
+// Notifier is the interface the wraps Subscribe and Unsubscribe. An
 // implementation of this interface notifies subscribers of nodes being added,
 // updated or deleted.
 type Notifier interface {
 	// Subscribe adds the given NodeHandler to the list of subscribers that are
-	// notified of node changes. Upcon call to this method, the NodeHandler is
+	// notified of node changes. Upon call to this method, the NodeHandler is
 	// being notified of all nodes that are already in the cluster by calling
 	// the NodeHandler's NodeAdd callback.
 	Subscribe(datapath.NodeHandler)
@@ -183,7 +183,7 @@ func NewManager(name string, dp datapath.NodeHandler, ipcache IPCache, c Configu
 		Subsystem: "nodes",
 		Name:      name + "_events_received_total",
 		Help:      "Number of node events received",
-	}, []string{"eventType", "source"})
+	}, []string{"eventType", "event_type", "source"}) //TODO(sayboras): Remove deprecated tag eventType in 1.10
 
 	m.metricNumNodes = prometheus.NewGauge(prometheus.GaugeOpts{
 		Namespace: metrics.Namespace,
@@ -196,7 +196,7 @@ func NewManager(name string, dp datapath.NodeHandler, ipcache IPCache, c Configu
 		Namespace: metrics.Namespace,
 		Subsystem: "nodes",
 		Name:      name + "_datapath_validations_total",
-		Help:      "Number of validation calls to implement the datapath implemention of a node",
+		Help:      "Number of validation calls to implement the datapath implementation of a node",
 	})
 
 	err := metrics.RegisterList([]prometheus.Collector{m.metricDatapathValidations, m.metricEventsReceived, m.metricNumNodes})
@@ -382,7 +382,8 @@ func (m *Manager) NodeUpdated(n nodeTypes.Node) {
 	m.mutex.Lock()
 	entry, oldNodeExists := m.nodes[nodeIdentity]
 	if oldNodeExists {
-		m.metricEventsReceived.WithLabelValues("update", string(n.Source)).Inc()
+		//TODO(sayboras): Remove deprecated metric in 1.10
+		m.metricEventsReceived.WithLabelValues("update", "update", string(n.Source)).Inc()
 
 		if !source.AllowOverwrite(entry.node.Source, n.Source) {
 			m.mutex.Unlock()
@@ -400,7 +401,8 @@ func (m *Manager) NodeUpdated(n nodeTypes.Node) {
 		}
 		entry.mutex.Unlock()
 	} else {
-		m.metricEventsReceived.WithLabelValues("add", string(n.Source)).Inc()
+		//TODO(sayboras): Remove deprecated metric in 1.10
+		m.metricEventsReceived.WithLabelValues("add", "add", string(n.Source)).Inc()
 		m.metricNumNodes.Inc()
 
 		entry = &nodeEntry{node: n}
@@ -418,10 +420,11 @@ func (m *Manager) NodeUpdated(n nodeTypes.Node) {
 
 // NodeDeleted is called after a node has been deleted. It removes the node
 // from the manager if the node is still owned by the source of which the event
-// orgins from. If the node was removed, NodeDelete() is invoked of the
+// origins from. If the node was removed, NodeDelete() is invoked of the
 // datapath interface.
 func (m *Manager) NodeDeleted(n nodeTypes.Node) {
-	m.metricEventsReceived.WithLabelValues("delete", string(n.Source)).Inc()
+	//TODO(sayboras): Remove deprecated metric in 1.10
+	m.metricEventsReceived.WithLabelValues("delete", "delete", string(n.Source)).Inc()
 
 	log.Debugf("Received node delete event from %s", n.Source)
 
@@ -511,7 +514,7 @@ func (m *Manager) GetNodes() map[nodeTypes.Identity]nodeTypes.Node {
 	return nodes
 }
 
-// DeleteAllNodes deletes all nodes from the node maanger.
+// DeleteAllNodes deletes all nodes from the node manager.
 func (m *Manager) DeleteAllNodes() {
 	m.mutex.Lock()
 	for _, entry := range m.nodes {
diff --git a/pkg/option/config.go b/pkg/option/config.go
index 2c10a2cb85..be33770400 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -266,6 +266,9 @@ const (
 	// conflicting marks.
 	EnableIdentityMark = "enable-identity-mark"
 
+	// EnableBandwidthManager enables EDT-based pacing
+	EnableBandwidthManager = "enable-bandwidth-manager"
+
 	// LibDir enables the directory path to store runtime build environment
 	LibDir = "lib-dir"
 
@@ -1744,6 +1747,9 @@ type DaemonConfig struct {
 	// features in BPF datapath
 	KubeProxyReplacement string
 
+	// EnableBandwidthManager enables EDT-based pacing
+	EnableBandwidthManager bool
+
 	// EnableExternalIPs enables implementation of k8s services with externalIPs in datapath
 	EnableExternalIPs bool
 
@@ -2334,6 +2340,7 @@ func (c *DaemonConfig) Populate() {
 	c.EnableAutoProtectNodePortRange = viper.GetBool(EnableAutoProtectNodePortRange)
 	c.KubeProxyReplacement = viper.GetString(KubeProxyReplacement)
 	c.EnableSessionAffinity = viper.GetBool(EnableSessionAffinity)
+	c.EnableBandwidthManager = viper.GetBool(EnableBandwidthManager)
 	c.EnableHostFirewall = viper.GetBool(EnableHostFirewall)
 	c.EncryptInterface = viper.GetString(EncryptInterface)
 	c.EncryptNode = viper.GetBool(EncryptNode)
diff --git a/pkg/policy/api/selector.go b/pkg/policy/api/selector.go
index fb46cc9fab..69db711610 100644
--- a/pkg/policy/api/selector.go
+++ b/pkg/policy/api/selector.go
@@ -185,6 +185,7 @@ func labelSelectorToRequirements(labelSelector *slim_metav1.LabelSelector) *k8sL
 	selector, err := slim_metav1.LabelSelectorAsSelector(labelSelector)
 	if err != nil {
 		metrics.PolicyImportErrors.Inc()
+		metrics.PolicyImportErrorsTotal.Inc()
 		log.WithError(err).WithField(logfields.EndpointLabelSelector,
 			logfields.Repr(labelSelector)).Error("unable to construct selector in label selector")
 		return nil
diff --git a/pkg/policy/groups/actions.go b/pkg/policy/groups/actions.go
index d4d2c5022d..fd03e1e18d 100644
--- a/pkg/policy/groups/actions.go
+++ b/pkg/policy/groups/actions.go
@@ -151,6 +151,7 @@ func addDerivativeCNP(ctx context.Context, cnp *cilium_v2.CiliumNetworkPolicy) e
 			break
 		}
 		metrics.PolicyImportErrors.Inc()
+		metrics.PolicyImportErrorsTotal.Inc()
 		scopedLog.WithError(derivativeErr).Error("Cannot create derivative rule. Installing deny-all rule.")
 		statusErr := updateDerivativeStatus(cnp, derivativeCNP.ObjectMeta.Name, derivativeErr)
 		if statusErr != nil {
@@ -164,6 +165,7 @@ func addDerivativeCNP(ctx context.Context, cnp *cilium_v2.CiliumNetworkPolicy) e
 		statusErr := updateDerivativeStatus(cnp, derivativeCNP.ObjectMeta.Name, err)
 		if statusErr != nil {
 			metrics.PolicyImportErrors.Inc()
+			metrics.PolicyImportErrorsTotal.Inc()
 			scopedLog.WithError(err).Error("Cannot update CNP status for derivative policy")
 		}
 		return statusErr
diff --git a/pkg/policy/l4.go b/pkg/policy/l4.go
index ca8f8e8f78..6f2ba5e73b 100644
--- a/pkg/policy/l4.go
+++ b/pkg/policy/l4.go
@@ -346,10 +346,9 @@ func (l4 *L4Filter) ToMapState(policyOwner PolicyOwner, direction trafficdirecti
 //
 // The caller is responsible for making sure the same identity is not
 // present in both 'added' and 'deleted'.
-func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity) {
 	log.WithFields(logrus.Fields{
 		logfields.EndpointSelector: selector,
-		logfields.PolicyID:         selections,
 		logfields.AddedPolicyID:    added,
 		logfields.DeletedPolicyID:  deleted,
 	}).Debug("identities selected by L4Filter updated")
diff --git a/pkg/policy/repository.go b/pkg/policy/repository.go
index c224140a21..4cfdd0b1fe 100644
--- a/pkg/policy/repository.go
+++ b/pkg/policy/repository.go
@@ -345,8 +345,8 @@ func (p *Repository) AddListLocked(rules api.Rules) (ruleSlice, uint64) {
 
 	p.rules = append(p.rules, newList...)
 	p.BumpRevision()
+	metrics.Policy.Add(float64(len(newList)))
 	metrics.PolicyCount.Add(float64(len(newList)))
-
 	return newList, p.GetRevision()
 }
 
@@ -448,6 +448,7 @@ func (p *Repository) DeleteByLabelsLocked(lbls labels.LabelArray) (ruleSlice, ui
 	if deleted > 0 {
 		p.BumpRevision()
 		p.rules = new
+		metrics.Policy.Sub(float64(deleted))
 		metrics.PolicyCount.Sub(float64(deleted))
 	}
 
diff --git a/pkg/policy/selectorcache.go b/pkg/policy/selectorcache.go
index 3a500046e2..f413948eea 100644
--- a/pkg/policy/selectorcache.go
+++ b/pkg/policy/selectorcache.go
@@ -106,7 +106,7 @@ type CachedSelectionUser interface {
 	//
 	// The caller is responsible for making sure the same identity is not
 	// present in both 'added' and 'deleted'.
-	IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity)
+	IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity)
 }
 
 // identitySelector is the internal interface for all selectors in the
@@ -378,7 +378,7 @@ type fqdnSelector struct {
 func (f *fqdnSelector) notifyUsers(added, deleted []identity.NumericIdentity) {
 	for user := range f.users {
 		// pass 'f' to the user as '*fqdnSelector'
-		user.IdentitySelectionUpdated(f, f.GetSelections(), added, deleted)
+		user.IdentitySelectionUpdated(f, added, deleted)
 	}
 }
 
@@ -437,7 +437,7 @@ type labelIdentitySelector struct {
 func (l *labelIdentitySelector) notifyUsers(added, deleted []identity.NumericIdentity) {
 	for user := range l.users {
 		// pass 'l' to the user as '*labelIdentitySelector'
-		user.IdentitySelectionUpdated(l, l.GetSelections(), added, deleted)
+		user.IdentitySelectionUpdated(l, added, deleted)
 	}
 }
 
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 1219220ce5..e0039bd1fb 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -64,7 +64,7 @@ type healthServer interface {
 
 // monitorNotify is used to send update notifications to the monitor
 type monitorNotify interface {
-	SendNotification(typ monitorAPI.AgentNotification, text string) error
+	SendNotification(msg monitorAPI.AgentNotifyMessage) error
 }
 
 type svcInfo struct {
@@ -928,17 +928,13 @@ func (s *Service) notifyMonitorServiceUpsert(frontend lb.L3n4AddrID, backends []
 		be = append(be, b)
 	}
 
-	repr, err := monitorAPI.ServiceUpsertRepr(id, fe, be, string(svcType), string(svcTrafficPolicy), svcName, svcNamespace)
-	if err == nil {
-		s.monitorNotify.SendNotification(monitorAPI.AgentNotifyServiceUpserted, repr)
-	}
+	msg := monitorAPI.ServiceUpsertMessage(id, fe, be, string(svcType), string(svcTrafficPolicy), svcName, svcNamespace)
+	s.monitorNotify.SendNotification(msg)
 }
 
 func (s *Service) notifyMonitorServiceDelete(id lb.ID) {
 	if s.monitorNotify != nil {
-		if repr, err := monitorAPI.ServiceDeleteRepr(uint32(id)); err == nil {
-			s.monitorNotify.SendNotification(monitorAPI.AgentNotifyServiceDeleted, repr)
-		}
+		s.monitorNotify.SendNotification(monitorAPI.ServiceDeleteMessage(uint32(id)))
 	}
 }
 


Test patch:
diff --git a/.github/workflows/smoke-test.yaml b/.github/workflows/smoke-test.yaml
index 901ddfa6bb..582a63b1fd 100644
--- a/.github/workflows/smoke-test.yaml
+++ b/.github/workflows/smoke-test.yaml
@@ -135,7 +135,11 @@ jobs:
              --set global.hostPort.enabled=true \
              --set config.bpfMasquerade=false \
              --set config.ipam=kubernetes \
-             --set global.pullPolicy=Never
+             --set global.pullPolicy=Never \
+             --set global.prometheus.enabled=true \
+             --set global.operatorPrometheus.enabled=true \
+             --set global.hubble.enabled=true \
+             --set global.hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,http}"
 
           kubectl wait -n kube-system --for=condition=Ready --all pod --timeout=5m
           # To make sure that cilium CRD is available (default timeout is 5m)
@@ -147,6 +151,18 @@ jobs:
           kubectl apply -f ${{ env.CONFORMANCE_TEMPLATE }}
           kubectl wait --for=condition=Available --all deployment --timeout=${{ env.TIMEOUT }}
 
+      - name: Check prometheus metrics
+        if: ${{ success() }}
+        env:
+          DEPRECATED_METRICS: cilium_bpf_map_ops_total|cilium_endpoint_count|cilium_identity_count|cilium_endpoint_regenerations|cilium_k8s_client_api_calls_counter|cilium_nodes_all_events_received_total|cilium_policy_count|cilium_policy_import_errors
+        run: |
+          cd $HOME
+          cilium_pod=$(kubectl -n kube-system get po -o name --field-selector=status.phase==Running -l 'k8s-app=cilium' -o jsonpath='{.items[0].metadata.name}' )
+          kubectl -n kube-system exec $cilium_pod -- sh -c "apt update && apt install curl -y"
+          kubectl -n kube-system exec $cilium_pod -- curl http://localhost:9090/metrics > metrics.prom
+          go get -u github.com/prometheus/prometheus/cmd/promtool
+          egrep -v "${{ env.DEPRECATED_METRICS }}" metrics.prom | $HOME/go/bin/promtool check metrics
+
       - name: Dump cilium related logs and events
         if: ${{ failure() }}
         run: |
diff --git a/daemon/cmd/daemon_test.go b/daemon/cmd/daemon_test.go
index 6a566f3ff6..0c11bd185d 100644
--- a/daemon/cmd/daemon_test.go
+++ b/daemon/cmd/daemon_test.go
@@ -61,7 +61,7 @@ type DaemonSuite struct {
 	OnGetPolicyRepository  func() *policy.Repository
 	OnQueueEndpointBuild   func(ctx context.Context, epID uint64) (func(), error)
 	OnGetCompilationLock   func() *lock.RWMutex
-	OnSendNotification     func(typ monitorAPI.AgentNotification, text string) error
+	OnSendNotification     func(typ monitorAPI.AgentNotifyMessage) error
 	OnGetCIDRPrefixLengths func() ([]int, []int)
 
 	// Metrics
@@ -254,9 +254,9 @@ func (ds *DaemonSuite) GetCompilationLock() *lock.RWMutex {
 	panic("GetCompilationLock should not have been called")
 }
 
-func (ds *DaemonSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (ds *DaemonSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	if ds.OnSendNotification != nil {
-		return ds.OnSendNotification(typ, text)
+		return ds.OnSendNotification(msg)
 	}
 	panic("SendNotification should not have been called")
 }
diff --git a/pkg/endpoint/endpoint_test.go b/pkg/endpoint/endpoint_test.go
index 49507c9586..d39275196b 100644
--- a/pkg/endpoint/endpoint_test.go
+++ b/pkg/endpoint/endpoint_test.go
@@ -68,7 +68,7 @@ type EndpointSuite struct {
 	OnQueueEndpointBuild      func(ctx context.Context, epID uint64) (func(), error)
 	OnRemoveFromEndpointQueue func(epID uint64)
 	OnGetCompilationLock      func() *lock.RWMutex
-	OnSendNotification        func(typ monitorAPI.AgentNotification, text string) error
+	OnSendNotification        func(msg monitorAPI.AgentNotifyMessage) error
 
 	// Metrics
 	collectors []prometheus.Collector
@@ -110,7 +110,7 @@ func (s *EndpointSuite) GetCompilationLock() *lock.RWMutex {
 	return nil
 }
 
-func (s *EndpointSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (s *EndpointSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
diff --git a/pkg/endpoint/redirect_test.go b/pkg/endpoint/redirect_test.go
index 1a7669a8af..7197287688 100644
--- a/pkg/endpoint/redirect_test.go
+++ b/pkg/endpoint/redirect_test.go
@@ -114,7 +114,7 @@ func (d *DummyOwner) GetCIDRPrefixLengths() (s6, s4 []int) {
 }
 
 // SendNotification does nothing.
-func (d *DummyOwner) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (d *DummyOwner) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
diff --git a/pkg/endpointmanager/manager_test.go b/pkg/endpointmanager/manager_test.go
index 566d9bfc86..9a78b0f5e0 100644
--- a/pkg/endpointmanager/manager_test.go
+++ b/pkg/endpointmanager/manager_test.go
@@ -83,7 +83,7 @@ func (s *EndpointManagerSuite) GetCIDRPrefixLengths() (s6, s4 []int) {
 	return nil, nil
 }
 
-func (s *EndpointManagerSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (s *EndpointManagerSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
diff --git a/pkg/envoy/server_test.go b/pkg/envoy/server_test.go
index aca398e145..c8b9aa344a 100644
--- a/pkg/envoy/server_test.go
+++ b/pkg/envoy/server_test.go
@@ -39,7 +39,7 @@ type ServerSuite struct{}
 
 type DummySelectorCacheUser struct{}
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 var (
diff --git a/pkg/fqdn/dnsproxy/proxy_test.go b/pkg/fqdn/dnsproxy/proxy_test.go
index 0073aa8eb6..e23f2b7854 100644
--- a/pkg/fqdn/dnsproxy/proxy_test.go
+++ b/pkg/fqdn/dnsproxy/proxy_test.go
@@ -90,7 +90,7 @@ func (s *DNSProxyTestSuite) GetCIDRPrefixLengths() (s6, s4 []int) {
 	return nil, nil
 }
 
-func (s *DNSProxyTestSuite) SendNotification(typ monitorAPI.AgentNotification, text string) error {
+func (s *DNSProxyTestSuite) SendNotification(msg monitorAPI.AgentNotifyMessage) error {
 	return nil
 }
 
@@ -136,7 +136,7 @@ func serveDNS(w dns.ResponseWriter, r *dns.Msg) {
 
 type DummySelectorCacheUser struct{}
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 // Setup identities, ports and endpoint IDs we will need
diff --git a/pkg/k8s/network_policy_test.go b/pkg/k8s/network_policy_test.go
index d244f9c40e..487b8121ba 100644
--- a/pkg/k8s/network_policy_test.go
+++ b/pkg/k8s/network_policy_test.go
@@ -107,7 +107,7 @@ func testNewPolicyRepository() *policy.Repository {
 	return repo
 }
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector policy.CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 func (s *K8sSuite) TestParseNetworkPolicyIngress(c *C) {
diff --git a/pkg/monitor/api/types_test.go b/pkg/monitor/api/types_test.go
index 72ba1dc713..457d1cd7c3 100644
--- a/pkg/monitor/api/types_test.go
+++ b/pkg/monitor/api/types_test.go
@@ -64,7 +64,7 @@ func testEqualityEndpoint(got, expected string, c *C) {
 	c.Assert(gotStruct, checker.DeepEquals, expectedStruct)
 }
 
-func (s *MonitorAPISuite) TestRulesRepr(c *C) {
+func (s *MonitorAPISuite) TestPolicyUpdateMessage(c *C) {
 	rules := api.Rules{
 		&api.Rule{
 			Labels: labels.LabelArray{
@@ -82,27 +82,32 @@ func (s *MonitorAPISuite) TestRulesRepr(c *C) {
 	for _, r := range rules {
 		labels = append(labels, r.Labels.GetModel()...)
 	}
-	repr, err := PolicyUpdateRepr(len(rules), labels, 1)
 
+	msg := PolicyUpdateMessage(len(rules), labels, 1)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityRules(repr, `{"labels":["unspec:key1=value1","unspec:key2=value2"],"revision":1,"rule_count":2}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyPolicyUpdated)
+	testEqualityRules(repr.Text, `{"labels":["unspec:key1=value1","unspec:key2=value2"],"revision":1,"rule_count":2}`, c)
 }
 
-func (s *MonitorAPISuite) TestRulesReprEmpty(c *C) {
-	repr, err := PolicyUpdateRepr(0, []string{}, 1)
-
+func (s *MonitorAPISuite) TestEmptyPolicyUpdateMessage(c *C) {
+	msg := PolicyUpdateMessage(0, []string{}, 1)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityRules(repr, `{"revision":1,"rule_count":0}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyPolicyUpdated)
+	testEqualityRules(repr.Text, `{"revision":1,"rule_count":0}`, c)
 }
 
-func (s *MonitorAPISuite) TestPolicyDeleteRepr(c *C) {
+func (s *MonitorAPISuite) TestPolicyDeleteMessage(c *C) {
 	lab := labels.LabelArray{
 		labels.NewLabel("key1", "value1", labels.LabelSourceUnspec),
 	}
 
-	repr, err := PolicyDeleteRepr(1, lab.GetModel(), 2)
+	msg := PolicyDeleteMessage(1, lab.GetModel(), 2)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityRules(repr, `{"labels":["unspec:key1=value1"],"revision":2,"rule_count":1}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyPolicyDeleted)
+	testEqualityRules(repr.Text, `{"labels":["unspec:key1=value1"],"revision":2,"rule_count":1}`, c)
 }
 
 type RegenError struct{}
@@ -131,24 +136,29 @@ func (MockEndpoint) GetK8sNamespace() string {
 	return ""
 }
 
-func (s *MonitorAPISuite) TestEndpointRegenRepr(c *C) {
+func (s *MonitorAPISuite) TestEndpointRegenMessage(c *C) {
 	e := MockEndpoint{}
 	rerr := RegenError{}
 
-	repr, err := EndpointRegenRepr(e, rerr)
+	msg := EndpointRegenMessage(e, rerr)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityEndpoint(repr, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"],"error":"RegenError"}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyEndpointRegenerateFail)
+	testEqualityEndpoint(repr.Text, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"],"error":"RegenError"}`, c)
 
-	repr, err = EndpointRegenRepr(e, nil)
+	msg = EndpointRegenMessage(e, nil)
+	repr, err = msg.ToJSON()
 	c.Assert(err, IsNil)
-	testEqualityEndpoint(repr, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"]}`, c)
+	c.Assert(repr.Type, Equals, AgentNotifyEndpointRegenerateSuccess)
+	testEqualityEndpoint(repr.Text, `{"id":10,"labels":["unspec:key1=value1","unspec:key2=value2"]}`, c)
 }
 
-func (s *MonitorAPISuite) TestTimeRepr(c *C) {
+func (s *MonitorAPISuite) TestStartMessage(c *C) {
 	t := time.Now()
 
-	repr, err := TimeRepr(t)
-
+	msg := StartMessage(t)
+	repr, err := msg.ToJSON()
 	c.Assert(err, IsNil)
-	c.Assert(repr, Equals, fmt.Sprintf(`{"time":"%s"}`, t.String()))
+	c.Assert(repr.Type, Equals, AgentNotifyStart)
+	c.Assert(repr.Text, Equals, fmt.Sprintf(`{"time":"%s"}`, t.String()))
 }
diff --git a/pkg/policy/selectorcache_test.go b/pkg/policy/selectorcache_test.go
index 7ed6c40a29..6ab3be9dc5 100644
--- a/pkg/policy/selectorcache_test.go
+++ b/pkg/policy/selectorcache_test.go
@@ -34,7 +34,7 @@ var _ = Suite(&SelectorCacheTestSuite{})
 
 type DummySelectorCacheUser struct{}
 
-func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (d *DummySelectorCacheUser) IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity) {
 }
 
 type cachedSelectionUser struct {
@@ -106,11 +106,13 @@ func (csu *cachedSelectionUser) RemoveSelector(sel CachedSelector) {
 	csu.c.Assert(csu.notifications, Equals, notifications)
 }
 
-func (csu *cachedSelectionUser) IdentitySelectionUpdated(selector CachedSelector, selections, added, deleted []identity.NumericIdentity) {
+func (csu *cachedSelectionUser) IdentitySelectionUpdated(selector CachedSelector, added, deleted []identity.NumericIdentity) {
 	csu.notifications++
 	csu.adds += len(added)
 	csu.deletes += len(deleted)
 
+	selections := selector.GetSelections()
+
 	// Validate added & deleted against the selections
 	for _, add := range added {
 		csu.c.Assert(haveNid(add, selections), Equals, true)