diff --git a/.sai.json b/.sai.json
index 7b08cf71e83173e63ff97a3b16dd0994cef91c42..26a2de8a2ef780f399a0e4cc8c5770c378fef163 100644
--- a/.sai.json
+++ b/.sai.json
@@ -54,7 +54,7 @@
 			"build": "mkdir build destdir; cd build; export LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib;export SAI_CPACK=\"-G ZIP\";export MACOSX_DEPLOYMENT_TARGET=10.15 ; cmake .. -DCMAKE_MAKE_PROGRAM=/usr/bin/make -DLWS_OPENSSL_INCLUDE_DIRS=/usr/local/opt/openssl@1.1/include -DLWS_OPENSSL_LIBRARIES=\"/usr/local/opt/openssl/lib/libssl.dylib;/usr/local/opt/openssl/lib/libcrypto.dylib\" ${cmake} && make -j4 && make -j DESTDIR=../destdir install && ctest -j2 --output-on-failure ${cpack}"
 		},
 		"netbsd-OSX-bigsur/aarch64-apple-m1/llvm": {
-			"build": "mkdir build destdir; cd build; export LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib;export SAI_CPACK=\"-G ZIP\";export MACOSX_DEPLOYMENT_TARGET=10.15 ; cmake .. -DLWS_WITH_SUL_DEBUGGING=1 -DCMAKE_SYSTEM_PREFIX_PATH=/opt/homebrew -DLWS_OPENSSL_INCLUDE_DIRS=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/include '-DLWS_OPENSSL_LIBRARIES=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libssl.dylib;/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libcrypto.dylib' -DLWS_WITH_MINIMAL_EXAMPLES=1 ${cmake} && make -j6 && rm -rf ../destdir && make -j DESTDIR=../destdir install && ctest -j3 --output-on-failure ${cpack}"
+			"build": "mkdir build destdir; cd build; export LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib;export SAI_CPACK=\"-G ZIP\";export MACOSX_DEPLOYMENT_TARGET=10.15 ; cmake .. -DLWS_WITH_SUL_DEBUGGING=1 -DCMAKE_SYSTEM_PREFIX_PATH=/opt/homebrew -DLWS_OPENSSL_INCLUDE_DIRS=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/include '-DLWS_OPENSSL_LIBRARIES=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libssl.dylib;/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libcrypto.dylib' ${cmake} && make -j6 && rm -rf ../destdir && make -j DESTDIR=../destdir install && ctest -j3 --output-on-failure ${cpack}"
 		},
 		"solaris/x86_64-amd/gcc": {
 			"build": "mkdir build destdir; cd build; export SAI_CPACK=\"-G ZIP\";cmake .. ${cmake} && make -j 4 && make install DESTDIR=../destdir && ctest -j2 --output-on-failure ${cpack}",
@@ -115,7 +115,7 @@
 		},
 		"default-noudp": {
 			"cmake":	"-DLWS_WITH_UDP=0",
-			"platforms":	"w10/x86_64-amd/msvc, w10/x86_64-amd/noptmsvc, freertos-linkit/arm32-m4-mt7697-usi/gcc, linux-ubuntu-2004/aarch64-a72-bcm2711-rpi4/gcc, w10/x86_64-amd/mingw32, w10/x86_64-amd/mingw64, netbsd/aarch64BE-bcm2837-a53/gcc, w10/x86_64-amd/wmbedtls-msvc"
+			"platforms":	"w10/x86_64-amd/msvc, w10/x86_64-amd/noptmsvc, freertos-linkit/arm32-m4-mt7697-usi/gcc, linux-ubuntu-2004/aarch64-a72-bcm2711-rpi4/gcc, w10/x86_64-amd/mingw32, w10/x86_64-amd/mingw64, netbsd/aarch64BE-bcm2837-a53/gcc, w10/x86_64-amd/wmbedtlsmsvc"
 		},
 		"esp32-heltec": {
 			"cmake":	"",
@@ -181,7 +181,10 @@
 			"cmake":	"-DLWS_WITH_SECURE_STREAMS=1 -DLWS_WITH_SECURE_STREAMS_PROXY_API=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_SECURE_STREAMS_AUTH_SIGV4=1",
 			"platforms":	"not w10/x86_64-amd/msvc, netbsd/aarch64BE-bcm2837-a53/gcc, openbsd/x86_64-amd/llvm, solaris/x86_64-amd/gcc"
 		},
-
+		"secure-streams-proxy-metrics": {
+			"cmake":	"-DLWS_WITH_SECURE_STREAMS=1 -DLWS_WITH_SECURE_STREAMS_PROXY_API=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_SECURE_STREAMS_AUTH_SIGV4=1 -DLWS_WITH_SYS_METRICS=1",
+			"platforms":	"not w10/x86_64-amd/msvc, netbsd/aarch64BE-bcm2837-a53/gcc"
+		},
 		"distro_recommended": { # minimal examples also needed for ctest
 			"cmake":	"-DLWS_WITH_DISTRO_RECOMMENDED=1 -DLWS_WITH_MINIMAL_EXAMPLES=1",
 			"platforms":	"not freebsd-12/x86_64-amd/llvm, not linkit-cross, not w10/x86_64-amd/msvc, linux-ubuntu-2004/aarch64-a72-bcm2711-rpi4/gcc, linux-fedora-32/riscv64-virt/gcc",
@@ -193,6 +196,11 @@
 			# no distro -devel package for libuv
 			"platforms":	"not linux-centos-8/x86_64-amd/gcc"
 		},
+		"lwsws-nometrics": {
+			"cmake":	"-DLWS_WITH_LWSWS=ON -DLWS_WITHOUT_EXTENSIONS=0 -DLWS_WITH_HTTP2=1 -DLWS_WITH_ACME=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_GENCRYPTO=1 -DLWS_WITH_JOSE=1 -DLWS_WITH_SYS_ASYNC_DNS=1 -DLWS_WITH_SYS_NTPCLIENT=1 -DLWS_WITH_SYS_METRICS=0",
+			# no distro -devel package for libuv
+			"platforms":	"not linux-centos-8/x86_64-amd/gcc"
+		},
 		"lwsws2": {
 			"cmake":	"-DLWS_WITH_LWSWS=ON -DLWS_WITHOUT_EXTENSIONS=0 -DLWS_WITH_HTTP2=1 -DLWS_WITH_ACME=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_LWS_DSH=1",
 			# no distro -devel package for libuv
@@ -207,6 +215,10 @@
 			# no distro -devel package for mbedtls
 			"platforms":	"not linux-centos-7/x86_64-amd/gcc, not linux-centos-8/x86_64-amd/gcc"
 		},
+		"mbedtls-metrics": {
+			"cmake":	"-DLWS_WITH_MBEDTLS=1 -DLWS_WITH_HTTP2=1 -DLWS_WITH_LWSWS=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_JOSE=1 -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_SYS_METRICS=1",
+			"platforms":	"not linux-centos-7/x86_64-amd/gcc, not linux-centos-8/x86_64-amd/gcc"
+		},
 		"noserver": {
 			"cmake":	"-DLWS_WITHOUT_SERVER=ON -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_SECURE_STREAMS=1",
 			"platforms":    "w10/x86_64-amd/msvc, w10/x86_64-amd/noptmsvc"
diff --git a/CMakeLists-implied-options.txt b/CMakeLists-implied-options.txt
index 6c98359cec4513a1cdc68037f4862c83b368f441..89e319e1fb52b212c34a529141c3af2d7be62985 100644
--- a/CMakeLists-implied-options.txt
+++ b/CMakeLists-implied-options.txt
@@ -83,7 +83,7 @@ if(LWS_WITH_DISTRO_RECOMMENDED)
 	set(LWS_WITH_SOCKS5 1)				# selfcontained
 	set(LWS_WITH_RANGES 1)				# selfcontained
 	set(LWS_WITH_ACME 1)				# selfcontained / tls
-	set(LWS_WITH_SERVER_STATUS 1)			# selfcontained
+	set(LWS_WITH_SYS_METRICS 1)			# selfcontained
 	set(LWS_WITH_GLIB 1)				# glib
 	set(LWS_WITH_LIBUV 1)				# libuv
 	set(LWS_WITH_LIBEV 1)				# libev
@@ -128,6 +128,7 @@ endif()
 if (LWS_WITH_SECURE_STREAMS_PROXY_API)
 	set(LWS_WITH_LWS_DSH 1)
 	set(LWS_WITH_UNIX_SOCK 1)
+	set(LWS_WITH_SYS_SMD 1)
 endif()
 
 if (NOT LWS_WITH_NETWORK)
@@ -210,7 +211,7 @@ if (LWS_WITH_LWSWS)
  set(LWS_WITH_LIBUV_INTERNAL 1)
  set(LWS_WITH_EVENT_LIBS 1) # implied by LIBUV_INTERNAL
  set(LWS_WITH_ACCESS_LOG 1)
- set(LWS_WITH_SERVER_STATUS 1)
+ set(LWS_WITH_SYS_METRICS 1)
  set(LWS_WITH_LEJP 1)
  set(LWS_WITH_LEJP_CONF 1)
  set(LWS_WITH_PEER_LIMITS 1)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9be767fc692200df5f5a090a558d52d070e46aa7..996a54511d2721ba2fc65ba6b36b2d6e4298d98c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -112,7 +112,6 @@ option(LWS_WITH_SOCKS5 "Allow use of SOCKS5 proxy on client connections" OFF)
 option(LWS_WITH_PEER_LIMITS "Track peers and restrict resources a single peer can allocate" OFF)
 option(LWS_WITH_ACCESS_LOG "Support generating Apache-compatible access logs" OFF)
 option(LWS_WITH_RANGES "Support http ranges (RFC7233)" OFF)
-option(LWS_WITH_SERVER_STATUS "Support json + jscript server monitoring" OFF)
 option(LWS_WITH_THREADPOOL "Managed worker thread pool support (relies on pthreads)" OFF)
 option(LWS_WITH_HTTP_STREAM_COMPRESSION "Support HTTP stream compression" OFF)
 option(LWS_WITH_HTTP_BROTLI "Also offer brotli http stream compression (requires LWS_WITH_HTTP_STREAM_COMPRESSION)" OFF)
@@ -134,6 +133,7 @@ else()
 	option(LWS_WITH_RFC6724 "Enable RFC6724 DNS result sorting" OFF)
 endif()
 option(LWS_WITH_SYS_FAULT_INJECTION "Enable fault injection support" OFF)
+option(LWS_WITH_SYS_METRICS "Lws Metrics API" OFF)
 
 #
 # Secure Streams
@@ -250,7 +250,6 @@ set(LWS_LOGGING_BITFIELD_CLEAR 0 CACHE STRING "Bitfield describing which log lev
 option(LWS_LOGS_TIMESTAMP "Timestamp at start of logs" ON)
 option(LWS_LOG_TAG_LIFECYCLE "Log tagged object lifecycle as NOTICE" ON)
 option(LWS_AVOID_SIGPIPE_IGN "Android 7+ reportedly needs this" OFF)
-option(LWS_WITH_STATS "Keep statistics of lws internal operations" OFF)
 option(LWS_WITH_JOSE "JSON Web Signature / Encryption / Keys (RFC7515/6/) API" OFF)
 option(LWS_WITH_GENCRYPTO "Enable support for Generic Crypto apis independent of TLS backend" OFF)
 option(LWS_WITH_SELFTESTS "Selftests run at context creation" OFF)
@@ -272,7 +271,6 @@ option(LWS_WITH_EXTERNAL_POLL "Support external POLL integration using callback
 option(LWS_WITH_LWS_DSH "Support lws_dsh_t Disordered Shared Heap" OFF)
 option(LWS_CLIENT_HTTP_PROXYING "Support external http proxies for client connections" ON)
 option(LWS_WITH_FILE_OPS "Support file operations vfs" ON)
-option(LWS_WITH_DETAILED_LATENCY "Record detailed latency stats for each read and write" OFF)
 option(LWS_WITH_UDP "Platform supports UDP" ON)
 option(LWS_WITH_SPAWN "Spawn subprocesses with piped stdin/out/stderr" OFF)
 option(LWS_WITH_FSMOUNT "Overlayfs and fallback mounting apis" OFF)
diff --git a/README.md b/README.md
index 3aa3c97b96da70686c96cc114de43f0d36284466..2013f436d583031b5257430fa5765f973da4fa93 100644
--- a/README.md
+++ b/README.md
@@ -378,7 +378,8 @@ with `api-tests/api-test-async-dns` minimal example.
 You can now opt to measure and store us-resolution statistics on effective
 latencies for client operations, and easily spool them to a file in a
 format suitable for gnuplot, or handle in your own callback.  Enable
-`-DLWS_WITH_DETAILED_LATENCY=1` in cmake to build it into lws.
+`-DLWS_WITH_DETAILED_LATENCY=1` in cmake to build it into lws. (NB 2021-01-12
+this has been replaced by the lws_metrics support)
 
 If you are concerned about operation latency or potential blocking from
 user code, or behaviour under load, or latency variability on specific
diff --git a/READMEs/README.detailed-latency.md b/READMEs/README.detailed-latency.md
deleted file mode 100644
index 29e29d03a6602483ae1c662816239307a827fd2e..0000000000000000000000000000000000000000
--- a/READMEs/README.detailed-latency.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# lws detailed latency
-
-![lws detailed latency example plot](../doc-assets/lws-detailed-latency-example.png)
-
-## Introduction
-
-lws has the capability to make detailed latency measurements and
-report them in realtime to a specified callback.
-
-A default callback is provided that renders the data as text in
-space-separated format suitable for gnuplot, to a specified file.
-
-## Configuring
-
-Enable `LWS_WITH_DETAILED_LATENCY` at cmake.
-
-Create your context with something similar to this
-
-```
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-results";
-#endif
-```
-
-`lws_det_lat_plot_cb` is provided by lws as a convenience to convert
-the stuct data provided at the callback interface to space-separated
-text data that is easy to process with shell commands and gnuplot.
-
-## `lws_det_lat_plot_cb` format
-
-```
-728239173547 N 23062 0 0 23062 0 0 0
-728239192554 C 18879 0 0 18879 0 0 0
-728239217894 T 25309 0 0 25309 0 0 0
-728239234998 r 0 0 0 0 271 172 256
-728239250611 r 0 0 0 0 69 934 4096
-728239255679 w 19 122 18 159 20 80 80
-728239275718 w 20 117 15 152 18 80 80
-728239295578 w 10 73 7 90 7 80 80
-728239315567 w 9 67 5 81 7 80 80
-728239335745 w 23 133 9 165 14 80 80
-...
-```
-
-Each event is shown in 9 columns
-
- - unix time in us
- - event type
-   - N = Name resolution
-   - C = TCP Connection
-   - T = TLS negotiation server
-   - t = TLS negotiation client
-   - r = Read
-   - w = Write
- - us duration, for w time client spent waiting to write
- - us duration, for w time data spent in transit to proxy
- - us duration, for w time proxy waited to send data
- - as a convenience, sum of last 3 columns above
- - us duration, time spent in callback
- - last 2 are actual / requested size in bytes
-
-## Processing captured data with ministat
-
-Eg, to summarize overall latencies on all captured writes
-
-```
- $ cat /tmp/lws-latency-results | grep " w " | cut -d' ' -f6 | ministat
-...
-    N           Min           Max        Median           Avg        Stddev
-x 1000            43           273           141       132.672     32.471693
-```
-
-## Processing captured data with gnuplot
-
-### Gnuplot plotting script
-
-Create a gnuplot script, eg myscript.gp
-
-```
-reset
-set term pngcairo enhanced nocrop font "OpenSans, 12" size 800,600#output terminal and file
-set output "lws-latency.png"
-#set yrange [0:10000]
-#to put an empty boundary around the
-#data inside an autoscaled graph.
-set offset graph 0.05,0.05,0.05,0.0
-set style fill transparent solid 0.5 #fillstyle
-set tics out nomirror
-set xlabel "event"
-set ylabel "latency (us)"
-set format x ""
-set title "Write latency"
-set key invert reverse Right inside nobox
-set key autotitle columnheader
-set style data histogram
-set style histogram rowstacked
-set style fill solid border -1
-set boxwidth 0.75
-set style fill solid 1.00 noborder
-set tic scale 0
-set grid ytics lc rgb "#505050"
-unset border
-unset xtics
-
-plot '/tmp/1' \
-	   using ($3 + $4 + $5):xtic(1)         w boxes lt rgbcolor "blue"  title 'prox wr wait', \
-	'' using ($3 + $4):xtic(1)         w boxes lt rgbcolor "green" title 'txfr to prox', \
-	'' using 3:xtic(1) w boxes lt rgbcolor "red"   title 'cli wri wait'
-```
-
-### gnuplot invocation
-
-```
- $ cat /tmp/lws-latency-results | grep " w " \>/tmp/1 ; gnuplot myscript.gp && eog lws-latency.png
-```
-
diff --git a/READMEs/README.lws_metrics.md b/READMEs/README.lws_metrics.md
new file mode 100644
index 0000000000000000000000000000000000000000..82cd2a525826a92cf66dfdc20c3b29d1af2bbbd4
--- /dev/null
+++ b/READMEs/README.lws_metrics.md
@@ -0,0 +1,245 @@
+## `lws_metrics`
+
+### Introduction
+
+`lws_metrics` records and aggregates **events** at all lws layers.
+
+There are three distinct parts:
+
+ - the architecture inside lws for collecting and aggregating / decimating the
+   events and maintaining statistics about them, these are lws_metric objects
+
+ - an external handler for forwarding aggregated metrics.  An lws_system ops
+   interface to pass on the aggregated metrics to an external backend.  lws
+   presents its own public metrics objects and leaves it to the external
+   code to have a shim to marry the lws metrics up to whatever is needed in the
+   metrics backend
+
+ - a policy for when to emit each type of aggregated information to the external
+   handler.  This can be specified in the generic Secure Streams policy, or
+   a linked-list of lws_metric_policy_t object passed it at context creation in
+   `info.metrics_policies`.
+
+The external backend interface code may itself make use of lws connectivity apis
+including Secure Streams itself, and lws metrics are available on that too.
+
+### `lws_metrics` policy-based reporting
+
+Normally metrics implementations are fixed at build-time and cannot change
+without a coordinated reflash of devices along with a change of backend schema.
+
+`lws_metrics` separates out the objects and code necessary to collect and
+aggregate the data cheaply, and the reporting policy that controls if, or how
+often, the results are reported to the external handler.
+
+![policy based metrics](/doc-assets/lws_metrics-policy.png)
+
+Metrics are created with a namespace name and the policy applies itself to those
+by listing the names, with wildcards allowed, the policy applies to, eg if
+specified in the Secure Streams JSON policy
+
+```
+	...
+	"metrics": [
+                {
+                        "name":         "tensecs",
+                        "us_schedule":  10000000,
+                        "report":	"cpu.*"
+                }, {
+                        "name":         "30secs",
+                        "us_schedule":  30000000,
+                        "report":       "n.cn.*, n.http.*, n.ss.*, vh.*"
+                }
+        ],
+        ...
+```
+
+Metrics that do not have a reporting policy do not report, but continue to
+aggregate measurements in case they are bound to a policy dynamically later.
+
+### Freeform metrics naming
+
+There is no predefined metrics schema, metrics objects, including those created
+by applications, can independently choose their own name in a namespace like
+"cpu.srv" or "n.cn.dns", and can set a prefix for all metrics names created in a
+context (by setting `info.metrics_prefix` at context creation time).
+
+This allows multiple processes in a single device to expose copies of the same
+metrics in an individually addressable way, eg, if the UI process specifies the
+prefix "ui", then its lws metrics like "cpu.srv" will actually be created as
+"ui.cpu.srv".
+
+Applications can freely define their own `lws_metrics` measurements with their
+own names in the namespace too, without central registration, and refer to those
+names in the reporting policy same as any other metric names.
+
+If the metrics backend requires a fixed schema, the mapping between the
+`lws_metrics` names and the backend schema indexes will be done in the
+`lws_system` external reporting api implementation alone.  Metrics objects
+contain a `void * backend_opaque` that is ignored by lws and can be set and
+read by the external reporting handler implementation to facilitate that.
+
+### Histogram metrics tagging
+
+Histogram metrics track differently-qualified results in the same metric, for
+example the metric `n.cn.failures` maintains separate result counts for all
+variations and kinds of failure.
+
+```
+[2021/03/01 06:34:05:6570] U: my_metric_report: ssproxy.n.cn.failures{ss="badcert_selfsigned",hostname="invalidca.badcert.warmcat.com",peer="46.105.127.147",tls="invalidca"} 2
+[2021/03/01 06:34:05:6573] U: my_metric_report: ssproxy.n.cn.failures{hostname="invalidca.badcert.warmcat.com",peer="46.105.127.147",tls="invalidca"} 1
+[2021/03/01 06:34:05:6576] U: my_metric_report: ssproxy.n.cn.failures{ss="badcert_expired",hostname="warmcat.com",peer="46.105.127.147",tls="expired"} 2
+[2021/03/01 06:34:05:6578] U: my_metric_report: ssproxy.n.cn.failures{hostname="warmcat.com",peer="46.105.127.147",tls="expired"} 1
+[2021/03/01 06:34:05:6580] U: my_metric_report: ssproxy.n.cn.failures{ss="badcert_hostname",hostname="hostname.badcert.warmcat.com",peer="46.105.127.147",tls="hostname"} 2
+[2021/03/01 06:34:05:6583] U: my_metric_report: ssproxy.n.cn.failures{hostname="hostname.badcert.warmcat.com",peer="46.105.127.147",tls="hostname"} 1
+[2021/03/01 06:34:05:6585] U: my_metric_report: ssproxy.n.cn.failures{dns="nores -2"} 8
+```
+
+The user handler for metrics is expected to iterate these, in the provided
+examples (eg, minimal-secure-streams-testsfail)
+
+```
+#if defined(LWS_WITH_SYS_METRICS)
+static int
+my_metric_report(lws_metric_pub_t *mp)
+{
+	lws_metric_bucket_t *sub = mp->u.hist.head;
+	char buf[192];
+
+	do {
+		if (lws_metrics_format(mp, &sub, buf, sizeof(buf)))
+			lwsl_user("%s: %s\n", __func__, buf);
+	} while ((mp->flags & LWSMTFL_REPORT_HIST) && sub);
+
+	/* 0 = leave metric to accumulate, 1 = reset the metric */
+
+	return 1;
+}
+
+static const lws_system_ops_t system_ops = {
+	.metric_report = my_metric_report,
+};
+
+#endif
+```
+
+### `lws_metrics` decimation
+
+Event information can easily be produced faster than it can be transmitted, or
+is useful to record if everything is working.  In the case that things are not
+working, then eventually the number of events that are unable to be forwarded
+to the backend would overwhelm the local storage.
+
+For that reason, the metrics objects are designed to absorb and summarize a
+potentially large number of events cheaply by aggregating them, so even extreme
+situations can be tracked meaningfully inbetween dumps to the backend.
+
+There are two approaches:
+
+ - "aggregation": decimate keeping a uint64 mean + sum, along with a max and min
+ 
+ - "histogram": keep a linked-list of different named buckets, with a 64-bit
+   counter for the number of times an event in each bucket was observed
+
+A single metric aggregation object has separate "go / no-go" counters, since
+most operations can fail, and failing operations act differently.
+
+`lws_metrics` 'aggregation' supports decimation by
+
+ - a mean of a 64-bit event metric, separate for go and no-go events
+ - counters of go and no-go events
+ - a min and max of the metric
+ - keeping track of when the sample period started
+
+![metrics decimation](/doc-assets/lws_metrics-decimation.png)
+
+In addition, the policy defines a percentage variance from the mean that
+optionally qualifies events to be reported individually.
+
+The `lws_metrics` 'histogram' allows monitoring of different outcomes to
+produce counts of each outcome in the "bucket".  
+
+### `lws_metrics` flags
+
+When the metrics object is created, flags are used to control how it will be
+used and consumed.
+
+For example to create a histogram metrics object rather than the default
+aggregation type, you would give the flag `LWSMTFL_REPORT_HIST` at creation
+time.
+
+|Flag|Meaning|
+|---|---|
+|`LWSMTFL_REPORT_OUTLIERS`|track outliers and report them internally|
+|`LWSMTFL_REPORT_OUTLIERS_OOB`|report each outlier externally as they happen|
+|`LWSMTFL_REPORT_INACTIVITY_AT_PERIODIC`|explicitly externally report no activity at periodic cb, by default no events in the period is just not reported|
+|`LWSMTFL_REPORT_MEAN`|the mean is interesting for this metric|
+|`LWSMTFL_REPORT_ONLY_GO`|no-go pieces invalid and should be ignored, used for simple counters|
+|`LWSMTFL_REPORT_DUTY_WALLCLOCK_US`|the aggregated sum or mean can be compared to wallclock time| 
+|`LWSMTFL_REPORT_HIST`|object is a histogram (else aggregator)|
+
+### Built-in lws-layer metrics
+
+lws creates and maintains various well-known metrics when you enable build
+with cmake `-DLWS_WITH_SYS_METRICS=1`:
+
+#### Aggregation metrics
+|metric name|scope|type|meaning|
+---|---|---|---|
+`cpu.svc`|context|monotonic over time|time spent servicing, outside of event loop wait|
+`n.cn.dns`|context|go/no-go mean|duration of blocking libc DNS lookup|
+`n.cn.adns`|context|go/no-go mean|duration of SYS_ASYNC_DNS lws DNS lookup|
+`n.cn.tcp`|context|go/no-go mean|duration of tcp connection until accept|
+`n.cn.tls`|context|go/no-go mean|duration of tls connection until accept|
+`n.http.txn`|context|go (2xx)/no-go mean|duration of lws http transaction|
+`n.ss.conn`|context|go/no-go mean|duration of Secure Stream transaction|
+`n.ss.cliprox.conn`|context|go/no-go mean|time taken for client -> proxy connection|
+`vh.[vh-name].rx`|vhost|go/no-go sum|received data on the vhost|
+`vh.[vh-name].tx`|vhost|go/no-go sum|transmitted data on the vhost|
+
+#### Histogram metrics
+|metric name|scope|type|meaning|
+|---|---|---|---|
+`n.cn.failures`|context|histogram|Histogram of connection attempt failure reasons|
+
+#### Connection failure histogram buckets
+|Bucket name|Meaning|
+|---|---|
+`tls/invalidca`|Peer certificate CA signature missing or not trusted|
+`tls/hostname`|Peer certificate CN or SAN doesn't match the endpoint we asked for|
+`tls/notyetvalid`|Peer certificate start date is in the future (time wrong?)|
+`tls/expired`|Peer certificate is expiry date is in the past|
+`dns/badsrv`|No DNS result because couldn't talk to the server|
+`dns/nxdomain`|No DNS result because server says no result|
+
+The `lws-minimal-secure-streams` example is able to report the aggregated
+metrics at the end of execution, eg
+
+```
+[2021/01/13 11:47:19:9145] U: my_metric_report: cpu.svc: 137.045ms / 884.563ms (15%)
+[2021/01/13 11:47:19:9145] U: my_metric_report: n.cn.dns: Go: 4, mean: 3.792ms, min: 2.470ms, max: 5.426ms
+[2021/01/13 11:47:19:9145] U: my_metric_report: n.cn.tcp: Go: 4, mean: 40.633ms, min: 17.107ms, max: 94.560ms
+[2021/01/13 11:47:19:9145] U: my_metric_report: n.cn.tls: Go: 3, mean: 91.232ms, min: 30.322ms, max: 204.635ms
+[2021/01/13 11:47:19:9145] U: my_metric_report: n.http.txn: Go: 4, mean: 63.089ms, min: 20.184ms, max: 125.474ms
+[2021/01/13 11:47:19:9145] U: my_metric_report: n.ss.conn: Go: 4, mean: 161.740ms, min: 42.937ms, max: 429.510ms
+[2021/01/13 11:47:19:9145] U: my_metric_report: vh._ss_default.rx: Go: (1) 102, NoGo: (1) 0
+[2021/01/13 11:47:19:9145] U: my_metric_report: vh.le_via_dst.rx: Go: (22) 28.165Ki
+[2021/01/13 11:47:19:9145] U: my_metric_report: vh.le_via_dst.tx: Go: (1) 267
+[2021/01/13 11:47:19:9145] U: my_metric_report: vh.api_amazon_com.rx: Go: (1) 1.611Ki, NoGo: (1) 0
+[2021/01/13 11:47:19:9145] U: my_metric_report: vh.api_amazon_com.tx: Go: (3) 1.505Ki
+```
+
+lws-minimal-secure-stream-testsfail which tests various kinds of connection failure
+reports histogram results like this
+
+```
+[2021/01/15 13:10:16:0933] U: my_metric_report: n.cn.failures: tot: 36, [ tls/invalidca: 5, tls/expired: 5, tls/hostname: 5, dns/nxdomain: 21 ]
+```
+
+## Support for openmetrics
+
+Openmetrics https://tools.ietf.org/html/draft-richih-opsawg-openmetrics-00
+defines a textual metrics export format comaptible with Prometheus.  Lws
+provides a protocol plugin in `./plugins/protocol_lws_openmetrics_export`
+that enables direct export for prometheus scraping, and also protocols to
+proxy openmetrics export for unreachable servers.
diff --git a/cmake/lws_config.h.in b/cmake/lws_config.h.in
index 3d4b867dab2a0c5726dabbb4ffa00f8dbaf6ccc4..cd9aa2b3b0327fb1b65c6be6ae604cfa3381b075 100644
--- a/cmake/lws_config.h.in
+++ b/cmake/lws_config.h.in
@@ -195,6 +195,7 @@
 #cmakedefine LWS_WITH_SQLITE3
 #cmakedefine LWS_WITH_SYS_DHCP_CLIENT
 #cmakedefine LWS_WITH_SYS_FAULT_INJECTION
+#cmakedefine LWS_WITH_SYS_METRICS
 #cmakedefine LWS_WITH_SYS_NTPCLIENT
 #cmakedefine LWS_WITH_SYS_STATE
 #cmakedefine LWS_WITH_THREADPOOL
diff --git a/doc-assets/lws_metrics-decimation.png b/doc-assets/lws_metrics-decimation.png
new file mode 100644
index 0000000000000000000000000000000000000000..e791608cd03c1598543d096474e477e1a2b19cd9
Binary files /dev/null and b/doc-assets/lws_metrics-decimation.png differ
diff --git a/doc-assets/lws_metrics-policy.png b/doc-assets/lws_metrics-policy.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1d5766ca00e84c4fadedccddb46967c6fc86273
Binary files /dev/null and b/doc-assets/lws_metrics-policy.png differ
diff --git a/include/libwebsockets.h b/include/libwebsockets.h
index ae4ab6b38bd60f3dcb6f7fa7c1c51ec51a3b45bd..63dfa4f32f8cff6c48a491952b1c764b961466d0 100644
--- a/include/libwebsockets.h
+++ b/include/libwebsockets.h
@@ -575,8 +575,8 @@ struct lws;
 #include <libwebsockets/lws-retry.h>
 #include <libwebsockets/lws-adopt.h>
 #include <libwebsockets/lws-network-helper.h>
+#include <libwebsockets/lws-metrics.h>
 #include <libwebsockets/lws-system.h>
-#include <libwebsockets/lws-detailed-latency.h>
 #include <libwebsockets/lws-ws-close.h>
 #include <libwebsockets/lws-callbacks.h>
 #include <libwebsockets/lws-ws-state.h>
@@ -605,7 +605,6 @@ struct lws;
 #include <libwebsockets/lws-vfs.h>
 #endif
 #include <libwebsockets/lws-lejp.h>
-#include <libwebsockets/lws-stats.h>
 #include <libwebsockets/lws-struct.h>
 #include <libwebsockets/lws-threadpool.h>
 #include <libwebsockets/lws-tokenize.h>
diff --git a/include/libwebsockets/lws-context-vhost.h b/include/libwebsockets/lws-context-vhost.h
index b90876a014667bc8884a51e6ad72cb31674d4d1b..c72ded05a2c0e288621d1cafc88a901dca292c1d 100644
--- a/include/libwebsockets/lws-context-vhost.h
+++ b/include/libwebsockets/lws-context-vhost.h
@@ -245,6 +245,7 @@
 struct lws_plat_file_ops;
 struct lws_ss_policy;
 struct lws_ss_plugin;
+struct lws_metric_policy;
 
 typedef int (*lws_context_ready_cb_t)(struct lws_context *context);
 
@@ -725,13 +726,6 @@ struct lws_context_creation_info {
 	const lws_system_ops_t *system_ops;
 	/**< CONTEXT: hook up lws_system_ apis to system-specific
 	 * implementations */
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	det_lat_buf_cb_t detailed_latency_cb;
-	/**< CONTEXT: NULL, or callback to receive detailed latency information
-	 * collected for each read and write */
-	const char *detailed_latency_filepath;
-	/**< CONTEXT: NULL, or filepath to put latency data into */
-#endif
 	const lws_retry_bo_t *retry_and_idle_policy;
 	/**< VHOST: optional retry and idle policy to apply to this vhost.
 	 *   Currently only the idle parts are applied to the connections.
@@ -840,6 +834,17 @@ struct lws_context_creation_info {
 	 * (20 for FREERTOS) */
 #endif
 
+#if defined(LWS_WITH_SYS_METRICS)
+	const struct lws_metric_policy		*metrics_policies;
+	/**< non-SS policy metrics policies */
+	const char				*metrics_prefix;
+	/**< prefix for this context's metrics, used to distinguish metrics
+	 * pooled from different processes / applications, so, eg what would
+	 * be "cpu.svc" if this is NULL becomes "myapp.cpu.svc" is this is
+	 * set to "myapp".  Policies are applied using the name with the prefix,
+	 * if present.
+	 */
+#endif
 
 	/* Add new things just above here ---^
 	 * This is part of the ABI, don't needlessly break compatibility
diff --git a/include/libwebsockets/lws-detailed-latency.h b/include/libwebsockets/lws-detailed-latency.h
deleted file mode 100644
index 1b352c7a65349ca9995873fffaf9b1123a9d32d8..0000000000000000000000000000000000000000
--- a/include/libwebsockets/lws-detailed-latency.h
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * libwebsockets - small server side websockets and web server implementation
- *
- * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to
- * deal in the Software without restriction, including without limitation the
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- *
- * included from libwebsockets.h
- */
-
-enum {
-
-	/* types of latency, all nonblocking except name resolution */
-
-	LDLT_READ,	/* time taken to read LAT_DUR_PROXY_RX_TO_CLIENT_WRITE */
-	LDLT_WRITE,
-	LDLT_NAME_RESOLUTION, /* BLOCKING: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */
-	LDLT_CONNECTION, /* conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */
-	LDLT_TLS_NEG_CLIENT, /* tls conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */
-	LDLT_TLS_NEG_SERVER, /* tls conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */
-
-	LDLT_USER,
-
-	/* interval / duration elements in latencies array */
-
-	LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE				= 0,
-		/* us the client spent waiting to write to proxy */
-	LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX,
-		/* us the packet took to be received by proxy */
-	LAT_DUR_PROXY_PROXY_REQ_TO_WRITE,
-		/* us the proxy has to wait before it could write */
-	LAT_DUR_PROXY_RX_TO_ONWARD_TX,
-		/* us the proxy spent waiting to write to destination, or
-		 * if nonproxied, then time between write request and write */
-
-	LAT_DUR_USERCB, /* us duration of user callback */
-
-	LAT_DUR_STEPS /* last */
-};
-
-typedef struct lws_detlat {
-	lws_usec_t		earliest_write_req;
-	lws_usec_t		earliest_write_req_pre_write;
-		/**< use this for interval comparison */
-	const char		*aux; /* name for name resolution timing */
-	int			type;
-	uint32_t		latencies[LAT_DUR_STEPS];
-	size_t			req_size;
-	size_t			acc_size;
-} lws_detlat_t;
-
-typedef int (*det_lat_buf_cb_t)(struct lws_context *context,
-				const lws_detlat_t *d);
-
-/**
- * lws_det_lat_cb() - inject your own latency records
- *
- * \param context: the lws_context
- * \param d: the lws_detlat_t you have prepared
- *
- * For proxying or similar cases where latency information is available from
- * user code rather than lws itself, you can generate your own latency callback
- * events with your own lws_detlat_t.
- */
-
-LWS_VISIBLE LWS_EXTERN int
-lws_det_lat_cb(struct lws_context *context, lws_detlat_t *d);
-
-/*
- * detailed_latency_plot_cb() - canned save to file in plottable format cb
- *
- * \p context: the lws_context
- * \p d: the detailed latency event information
- *
- * This canned callback makes it easy to export the detailed latency information
- * to a file.  Just set the context creation members like this
- *
- * #if defined(LWS_WITH_DETAILED_LATENCY)
- *	info.detailed_latency_cb = lws_det_lat_plot_cb;
- *	info.detailed_latency_filepath = "/tmp/lws-latency-results";
- * #endif
- *
- * and you will get a file containing information like this
- *
- * 718823864615 N 10589 0 0 10589 0 0 0
- * 718823880837 C 16173 0 0 16173 0 0 0
- * 718823913063 T 32212 0 0 32212 0 0 0
- * 718823931835 r 0 0 0 0 232 30 256
- * 718823948757 r 0 0 0 0 40 30 256
- * 718823948799 r 0 0 0 0 83 30 256
- * 718823965602 r 0 0 0 0 27 30 256
- * 718823965617 r 0 0 0 0 43 30 256
- * 718823965998 r 0 0 0 0 12 28 256
- * 718823983887 r 0 0 0 0 74 3 4096
- * 718823986411 w 16 87 7 110 9 80 80
- * 718824006358 w 8 68 6 82 6 80 80
- *
- * which is easy to grep and pass to gnuplot.
- *
- * The columns are
- *
- *  - unix time in us
- *  - N = Name resolution, C = TCP Connection, T = TLS negotiation server,
- *    t = TLS negotiation client, r = Read, w = Write
- *  - us duration, for w time client spent waiting to write
- *  - us duration, for w time data spent in transit to proxy
- *  - us duration, for w time proxy waited to send data
- *  - as a convenience, sum of last 3 columns above
- *  - us duration, time spent in callback
- *  - last 2 are actual / requested size in bytes
- */
-LWS_VISIBLE LWS_EXTERN int
-lws_det_lat_plot_cb(struct lws_context *context, const lws_detlat_t *d);
-
-/**
- * lws_det_lat_active() - indicates if latencies are being measured
- *
- * \context: lws_context
- *
- * Returns 0 if latency measurement has not been set up (the callback is NULL).
- * Otherwise returns 1
- */
-LWS_VISIBLE LWS_EXTERN int
-lws_det_lat_active(struct lws_context *context);
diff --git a/include/libwebsockets/lws-lejp.h b/include/libwebsockets/lws-lejp.h
index 55c4dc0ee62ddc8b531c5583511389ea4bc6e2b5..6ea6d3dc2dd735b71ad554c64e13a67d04d39c87 100644
--- a/include/libwebsockets/lws-lejp.h
+++ b/include/libwebsockets/lws-lejp.h
@@ -182,7 +182,7 @@ typedef signed char (*lejp_callback)(struct lejp_ctx *ctx, char reason);
 #define LEJP_MAX_DEPTH 12
 #endif
 #ifndef LEJP_MAX_INDEX_DEPTH
-#define LEJP_MAX_INDEX_DEPTH 6
+#define LEJP_MAX_INDEX_DEPTH 8
 #endif
 #ifndef LEJP_MAX_PATH
 #define LEJP_MAX_PATH 128
diff --git a/include/libwebsockets/lws-metrics.h b/include/libwebsockets/lws-metrics.h
new file mode 100644
index 0000000000000000000000000000000000000000..4df7a266dceacfc758d66f2b5fcee2d74ea6d357
--- /dev/null
+++ b/include/libwebsockets/lws-metrics.h
@@ -0,0 +1,329 @@
+ /*
+ * libwebsockets - small server side websockets and web server implementation
+ *
+ * Copyright (C) 2010 - 2021 Andy Green <andy@warmcat.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Public apis related to metric collection and reporting
+ */
+
+/* lws_metrics public part */
+
+typedef uint64_t u_mt_t;
+
+enum {
+	LWSMTFL_REPORT_OUTLIERS				= (1 << 0),
+	/**< track outliers and report them internally */
+	LWSMTFL_REPORT_OOB				= (1 << 1),
+	/**< report events as they happen */
+	LWSMTFL_REPORT_INACTIVITY_AT_PERIODIC		= (1 << 2),
+	/**< explicitly externally report no activity at periodic cb, by
+	 * default no events in the period is just not reported */
+	LWSMTFL_REPORT_MEAN				= (1 << 3),
+	/**< average/min/max is meaningful, else only sum is meaningful */
+	LWSMTFL_REPORT_ONLY_GO				= (1 << 4),
+	/**< no-go pieces invalid */
+	LWSMTFL_REPORT_DUTY_WALLCLOCK_US		= (1 << 5),
+	/**< aggregate compares to wallclock us for duty cycle */
+	LWSMTFL_REPORT_HIST				= (1 << 6),
+	/**< our type is histogram (otherwise, sum / mean aggregation) */
+};
+
+/*
+ * lws_metrics_tag allows your object to accumulate OpenMetrics-style
+ * descriptive tags before accounting for it with a metrics object at the end.
+ *
+ * Tags should represent low entropy information that is likely to repeat
+ * identically, so, eg, http method name, not eg, latency in us which is
+ * unlikely to be seen the same twice.
+ *
+ * Tags are just a list of name=value pairs, used for qualifying the final
+ * metrics entry with decorations in additional dimensions.  For example,
+ * rather than keep individual metrics on methods, scheme, mountpoint, result
+ * code, you can keep metrics on http transactions only, and qualify the
+ * transaction metrics entries with tags that can be queried on the metrics
+ * backend to get the finer-grained information.
+ *
+ * http_srv{code="404",mount="/",method="GET",scheme="http"} 3
+ *
+ * For OpenMetrics the tags are converted to a { list } and appended to the base
+ * metrics name before using with actual metrics objects, the same set of tags
+ * on different transactions resolve to the same qualification string.
+ */
+
+typedef struct lws_metrics_tag {
+	lws_dll2_t	list;
+
+	const char	*name; /* tag, intended to be in .rodata, not copied */
+	/* overallocated value */
+} lws_metrics_tag_t;
+
+LWS_EXTERN LWS_VISIBLE int
+lws_metrics_tag_add(lws_dll2_owner_t *owner, const char *name, const char *val);
+
+#if defined(LWS_WITH_SYS_METRICS)
+/*
+ * wsi-specific version that also appends the tag value to the lifecycle tag
+ * used for logging the wsi identity
+ */
+LWS_EXTERN LWS_VISIBLE int
+lws_metrics_tag_wsi_add(struct lws *wsi, const char *name, const char *val);
+#else
+#define lws_metrics_tag_wsi_add(_a, _b, _c)
+#endif
+
+#if defined(LWS_WITH_SECURE_STREAMS)
+/*
+ * ss-specific version that also appends the tag value to the lifecycle tag
+ * used for logging the ss identity
+ */
+#if defined(LWS_WITH_SYS_METRICS)
+LWS_EXTERN LWS_VISIBLE int
+lws_metrics_tag_ss_add(struct lws_ss_handle *ss, const char *name, const char *val);
+#else
+#define lws_metrics_tag_ss_add(_a, _b, _c)
+#endif
+#endif
+
+LWS_EXTERN LWS_VISIBLE void
+lws_metrics_tags_destroy(lws_dll2_owner_t *owner);
+
+LWS_EXTERN LWS_VISIBLE size_t
+lws_metrics_tags_serialize(lws_dll2_owner_t *owner, char *buf, size_t len);
+
+LWS_EXTERN LWS_VISIBLE const char *
+lws_metrics_tag_get(lws_dll2_owner_t *owner, const char *name);
+
+/* histogram bucket */
+
+typedef struct lws_metric_bucket {
+	struct lws_metric_bucket	*next;
+	uint64_t			count;
+
+	/* name + NUL is overallocated */
+} lws_metric_bucket_t;
+
+/* get overallocated name of bucket from bucket pointer */
+#define lws_metric_bucket_name_len(_b) (*((uint8_t *)&(_b)[1]))
+#define lws_metric_bucket_name(_b) (((const char *)&(_b)[1]) + 1)
+
+/*
+ * These represent persistent local event measurements.  They may aggregate
+ * a large number of events inbetween external dumping of summaries of the
+ * period covered, in two different ways
+ *
+ * 1) aggregation by sum or mean, to absorb multiple scalar readings
+ *
+ *  - go / no-go ratio counting
+ *  - mean averaging for, eg, latencies
+ *  - min / max for averaged values
+ *  - period the stats covers
+ *
+ * 2) aggregation by histogram, to absorb a range of outcomes that may occur
+ *    multiple times
+ *
+ *  - add named buckets to histogram
+ *  - bucket has a 64-bit count
+ *  - bumping a bucket just increments the count if already exists, else adds
+ *    a new one with count set to 1
+ *
+ * The same type with a union covers both cases.
+ *
+ * The lws_system ops api that hooks lws_metrics up to a metrics backend is
+ * given a pointer to these according to the related policy, eg, hourly, or
+ * every event passed straight through.
+ */
+
+typedef struct lws_metric_pub {
+	const char		*name;
+	/**< eg, "n.cn.dns", "vh.myendpoint" */
+	void			*backend_opaque;
+	/**< ignored by lws, backend handler completely owns it */
+
+	lws_usec_t		us_first;
+	/**< us time metric started collecting, reset to us_dumped at dump */
+	lws_usec_t		us_last;
+	/**< 0, or us time last event, reset to 0 at last dump */
+	lws_usec_t		us_dumped;
+	/**< 0 if never, else us time of last dump to external api */
+
+	/* scope of data in .u is "since last dump" --> */
+
+	union {
+		/* aggregation, by sum or mean */
+
+		struct {
+			u_mt_t			sum[2];
+			/**< go, no-go summed for mean or plan sum */
+			u_mt_t			min;
+			/**< smallest individual measurement */
+			u_mt_t			max;
+			/**< largest individual measurement */
+
+			uint32_t		count[2];
+			/**< go, no-go count of measurements in sum */
+		} agg;
+
+		/* histogram with dynamic named buckets */
+
+		struct {
+			lws_metric_bucket_t	*head;
+			/**< first bucket in our bucket list */
+
+			uint64_t		total_count;
+			/**< total count in all of our buckets */
+			uint32_t		list_size;
+			/**< number of buckets in our bucket list */
+		} hist;
+	} u;
+
+	uint8_t			flags;
+
+} lws_metric_pub_t;
+
+LWS_EXTERN LWS_VISIBLE void
+lws_metrics_hist_bump_priv_tagged(lws_metric_pub_t *mt, lws_dll2_owner_t *tow,
+				  lws_dll2_owner_t *tow2);
+
+
+/*
+ * Calipers are a helper struct for implementing "hanging latency" detection,
+ * where setting the start time and finding the end time may happen in more than
+ * one place.
+ *
+ * There are convenience wrappers to eliminate caliper definitions and code
+ * cleanly if WITH_SYS_METRICS is disabled for the build.
+ */
+
+struct lws_metric;
+
+typedef struct lws_metric_caliper {
+	struct lws_dll2_owner	mtags_owner; /**< collect tags here during
+					      * caliper lifetime */
+	struct lws_metric	*mt; /**< NULL == inactive */
+	lws_usec_t		us_start;
+} lws_metric_caliper_t;
+
+#if defined(LWS_WITH_SYS_METRICS)
+#define lws_metrics_caliper_compose(_name) \
+		lws_metric_caliper_t _name;
+#define lws_metrics_caliper_bind(_name, _mt) \
+	{ if (_name.mt) { \
+		lwsl_err("caliper: overwrite %s\n", \
+				lws_metrics_priv_to_pub(_name.mt)->name); \
+		assert(0); } \
+	  _name.mt = _mt; _name.us_start = lws_now_usecs(); }
+#define lws_metrics_caliper_declare(_name, _mt) \
+	lws_metric_caliper_t _name = { .mt = _mt, .us_start = lws_now_usecs() }
+#define lws_metrics_caliper_report(_name, _go_nogo) \
+	{ if (_name.us_start) { lws_metric_event(_name.mt, _go_nogo, \
+			   (u_mt_t)(lws_now_usecs() - \
+					   _name.us_start)); \
+					  }  lws_metrics_caliper_done(_name);  }
+#define lws_metrics_caliper_report_hist(_name, pwsi) if (_name.mt) { \
+		lws_metrics_hist_bump_priv_tagged(lws_metrics_priv_to_pub(_name.mt), \
+						  &_name.mtags_owner, \
+						  pwsi ? &((pwsi)->cal_conn.mtags_owner) : NULL); \
+		lws_metrics_caliper_done(_name);  }
+
+#define lws_metrics_caliper_cancel(_name) { lws_metrics_caliper_done(_name); }
+#define lws_metrics_hist_bump(_mt, _name) \
+		lws_metrics_hist_bump_(_mt, _name)
+#define lws_metrics_hist_bump_priv(_mt, _name) \
+		lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_mt), _name)
+#define lws_metrics_caliper_done(_name) { \
+		_name.us_start = 0; _name.mt = NULL; \
+		lws_metrics_tags_destroy(&_name.mtags_owner); }
+#else
+#define lws_metrics_caliper_compose(_name)
+#define lws_metrics_caliper_bind(_name, _mt)
+#define lws_metrics_caliper_declare(_name, _mp)
+#define lws_metrics_caliper_report(_name, _go_nogo)
+#define lws_metrics_caliper_report_hist(_name, pwsiconn)
+#define lws_metrics_caliper_cancel(_name)
+#define lws_metrics_hist_bump(_mt, _name)
+#define lws_metrics_hist_bump_priv(_mt, _name)
+#define lws_metrics_caliper_done(_name)
+#endif
+
+/**
+ * lws_metrics_format() - helper to format a metrics object for logging
+ *
+ * \param pub: public part of metrics object
+ * \param buf: output buffer to place string in
+ * \param len: available length of \p buf
+ *
+ * Helper for describing the state of a metrics object as a human-readable
+ * string, accounting for how its flags indicate what it contains.  This is not
+ * how you would report metrics, but during development it can be useful to
+ * log them inbetween possibily long report intervals.
+ *
+ * It uses the metric's flags to adapt the format shown appropriately, eg,
+ * as a histogram if LWSMTFL_REPORT_HIST etc
+ */
+LWS_EXTERN LWS_VISIBLE int
+lws_metrics_format(lws_metric_pub_t *pub, lws_metric_bucket_t **sub,
+		   char *buf, size_t len);
+
+/**
+ * lws_metrics_hist_bump() - add or increment histogram bucket
+ *
+ * \param pub: public part of metrics object
+ * \param name: bucket name to increment
+ *
+ * Either increment the count of an existing bucket of the right name in the
+ * metrics object, or add a new bucket of the given name and set its count to 1.
+ *
+ * The metrics object must have been created with flag LWSMTFL_REPORT_HIST
+ *
+ * Normally, you will actually use the preprocessor wrapper
+ * lws_metrics_hist_bump() defined above, since this automatically takes care of
+ * removing itself from the build if WITH_SYS_METRICS is not defined, without
+ * needing any preprocessor conditionals.
+ */
+LWS_EXTERN LWS_VISIBLE int
+lws_metrics_hist_bump_(lws_metric_pub_t *pub, const char *name);
+
+LWS_VISIBLE LWS_EXTERN int
+lws_metrics_foreach(struct lws_context *ctx, void *user,
+		    int (*cb)(lws_metric_pub_t *pub, void *user));
+
+LWS_VISIBLE LWS_EXTERN int
+lws_metrics_hist_bump_describe_wsi(struct lws *wsi, lws_metric_pub_t *pub,
+				   const char *name);
+
+enum {
+	LMT_NORMAL = 0,	/* related to successful events */
+	LMT_OUTLIER,	/* related to successful events outside of bounds */
+
+	LMT_FAIL,	/* related to failed events */
+
+	LMT_COUNT,
+};
+
+typedef enum lws_metric_rpt {
+	LMR_PERIODIC = 0,	/* we are reporting on a schedule */
+	LMR_OUTLIER,		/* we are reporting the last outlier */
+} lws_metric_rpt_kind_t;
+
+#define METRES_GO	0
+#define METRES_NOGO	1
+
+
diff --git a/include/libwebsockets/lws-protocols-plugins.h b/include/libwebsockets/lws-protocols-plugins.h
index b39bf3b7c022f7f8b01ab29e6fc63778d1ffd9f2..ec9d8d8c86859c263d786be3141db3732a793b21 100644
--- a/include/libwebsockets/lws-protocols-plugins.h
+++ b/include/libwebsockets/lws-protocols-plugins.h
@@ -359,7 +359,22 @@ extern const struct lws_protocols lws_sshd_demo_protocols[1];
 extern const struct lws_protocols lws_acme_client_protocols[1];
 extern const struct lws_protocols client_loopback_test_protocols[1];
 extern const struct lws_protocols fulltext_demo_protocols[1];
+extern const struct lws_protocols lws_openmetrics_export_protocols[
+#if defined(LWS_WITH_SERVER) && defined(LWS_WITH_CLIENT) && defined(LWS_ROLE_WS)
+	4
+#else
+#if defined(LWS_WITH_SERVER)
+	3
+#else
+	1
+#endif
+#endif
+	];
 
+#define LWSOMPROIDX_DIRECT_HTTP_SERVER		0
+#define LWSOMPROIDX_PROX_HTTP_SERVER		1
+#define LWSOMPROIDX_PROX_WS_SERVER		2
+#define LWSOMPROIDX_PROX_WS_CLIENT		3
 
 #endif
 
diff --git a/include/libwebsockets/lws-secure-streams-policy.h b/include/libwebsockets/lws-secure-streams-policy.h
index d22b23e0040d3581a9ca0f73143f7a704637c3d2..f3924605dd88dfbc2a85090359ba1bdcb4ad3eb4 100644
--- a/include/libwebsockets/lws-secure-streams-policy.h
+++ b/include/libwebsockets/lws-secure-streams-policy.h
@@ -77,6 +77,25 @@ typedef struct lws_ss_plugin {
 } lws_ss_plugin_t;
 #endif
 
+/* the public, const metrics policy definition */
+
+typedef struct lws_metric_policy {
+	/* order of first two mandated by JSON policy parsing scope union */
+	const struct lws_metric_policy	*next;
+	const char			*name;
+
+	const char			*report;
+
+	/**< the metrics policy name in the policy, used to bind to it */
+	uint32_t			us_schedule;
+	/**< us interval between lws_system metrics api reports */
+
+	uint32_t			us_decay_unit;
+	/**< how many us to decay avg by half, 0 = no decay */
+	uint8_t				min_contributors;
+	/**< before we can judge something is an outlier */
+} lws_metric_policy_t;
+
 typedef struct lws_ss_x509 {
 	struct lws_ss_x509	*next;
 	const char		*vhost_name; /**< vhost name using cert ctx */
@@ -226,6 +245,7 @@ typedef struct lws_ss_policy {
 	const char		*payload_fmt;
 	const char		*socks5_proxy;
 	lws_ss_metadata_t	*metadata; /* linked-list of metadata */
+	const lws_metric_policy_t *metrics; /* linked-list of metric policies */
 	const lws_ss_auth_t	*auth; /* NULL or auth object we bind to */
 
 	/* protocol-specific connection policy details */
diff --git a/include/libwebsockets/lws-smd.h b/include/libwebsockets/lws-smd.h
index f5188b5afd879744589177c57f07dd8c1b56d7ba..50dbc9ecf556600b8845bdd752ac4a865f7cb0fa 100644
--- a/include/libwebsockets/lws-smd.h
+++ b/include/libwebsockets/lws-smd.h
@@ -52,6 +52,11 @@ enum {
 	 * Something happened on the network, eg, link-up or DHCP, or captive
 	 * portal state update
 	 */
+	LWSSMDCL_METRICS					= (1 << 3),
+	/**<
+	 * An SS client process is reporting a metric to the proxy (this class
+	 * is special in that it is not rebroadcast by the proxy)
+	 */
 
 	LWSSMDCL_USER_BASE_BITNUM				= 24
 };
diff --git a/include/libwebsockets/lws-stats.h b/include/libwebsockets/lws-stats.h
deleted file mode 100644
index ca9a4e625c0d35aafb806ec23ad926ae6cd72fa2..0000000000000000000000000000000000000000
--- a/include/libwebsockets/lws-stats.h
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * libwebsockets - small server side websockets and web server implementation
- *
- * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to
- * deal in the Software without restriction, including without limitation the
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- */
-
-/*
- * Stats are all uint64_t numbers that start at 0.
- * Index names here have the convention
- *
- *  _C_ counter
- *  _B_ byte count
- *  _MS_ millisecond count
- */
-
-enum {
-	LWSSTATS_C_CONNECTIONS, /**< count incoming connections */
-	LWSSTATS_C_API_CLOSE, /**< count calls to close api */
-	LWSSTATS_C_API_READ, /**< count calls to read from socket api */
-	LWSSTATS_C_API_LWS_WRITE, /**< count calls to lws_write API */
-	LWSSTATS_C_API_WRITE, /**< count calls to write API */
-	LWSSTATS_C_WRITE_PARTIALS, /**< count of partial writes */
-	LWSSTATS_C_WRITEABLE_CB_REQ, /**< count of writable callback requests */
-	LWSSTATS_C_WRITEABLE_CB_EFF_REQ, /**< count of effective writable callback requests */
-	LWSSTATS_C_WRITEABLE_CB, /**< count of writable callbacks */
-	LWSSTATS_C_SSL_CONNECTIONS_FAILED, /**< count of failed SSL connections */
-	LWSSTATS_C_SSL_CONNECTIONS_ACCEPTED, /**< count of accepted SSL connections */
-	LWSSTATS_C_SSL_ACCEPT_SPIN, /**< count of SSL_accept() attempts */
-	LWSSTATS_C_SSL_CONNS_HAD_RX, /**< count of accepted SSL conns that have had some RX */
-	LWSSTATS_C_TIMEOUTS, /**< count of timed-out connections */
-	LWSSTATS_C_SERVICE_ENTRY, /**< count of entries to lws service loop */
-	LWSSTATS_B_READ, /**< aggregate bytes read */
-	LWSSTATS_B_WRITE, /**< aggregate bytes written */
-	LWSSTATS_B_PARTIALS_ACCEPTED_PARTS, /**< aggreate of size of accepted write data from new partials */
-	LWSSTATS_US_SSL_ACCEPT_LATENCY_AVG, /**< aggregate delay in accepting connection */
-	LWSSTATS_US_WRITABLE_DELAY_AVG, /**< aggregate delay between asking for writable and getting cb */
-	LWSSTATS_US_WORST_WRITABLE_DELAY, /**< single worst delay between asking for writable and getting cb */
-	LWSSTATS_US_SSL_RX_DELAY_AVG, /**< aggregate delay between ssl accept complete and first RX */
-	LWSSTATS_C_PEER_LIMIT_AH_DENIED, /**< number of times we would have given an ah but for the peer limit */
-	LWSSTATS_C_PEER_LIMIT_WSI_DENIED, /**< number of times we would have given a wsi but for the peer limit */
-	LWSSTATS_C_CONNS_CLIENT, /**< attempted client conns */
-	LWSSTATS_C_CONNS_CLIENT_FAILED, /**< failed client conns */
-
-	/* Add new things just above here ---^
-	 * This is part of the ABI, don't needlessly break compatibility
-	 *
-	 * UPDATE stat_names in stats.c in sync with this!
-	 */
-	LWSSTATS_SIZE
-};
-
-#if defined(LWS_WITH_STATS)
-
-LWS_VISIBLE LWS_EXTERN uint64_t
-lws_stats_get(struct lws_context *context, int index);
-LWS_VISIBLE LWS_EXTERN void
-lws_stats_log_dump(struct lws_context *context);
-#else
-static LWS_INLINE uint64_t
-lws_stats_get(struct lws_context *context, int index) { (void)context; (void)index;  return 0; }
-static LWS_INLINE void
-lws_stats_log_dump(struct lws_context *context) { (void)context; }
-#endif
diff --git a/include/libwebsockets/lws-system.h b/include/libwebsockets/lws-system.h
index 1feef7967aca6441efa031310bc0945f40e6d999..5d57a1f7ab96263369691703262ecf3191af903e 100644
--- a/include/libwebsockets/lws-system.h
+++ b/include/libwebsockets/lws-system.h
@@ -1,7 +1,7 @@
  /*
  * libwebsockets - small server side websockets and web server implementation
  *
- * Copyright (C) 2010 - 2020 Andy Green <andy@warmcat.com>
+ * Copyright (C) 2010 - 2021 Andy Green <andy@warmcat.com>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
@@ -151,7 +151,6 @@ typedef enum {
 	LWS_CPD_NO_INTERNET,	/* we couldn't touch anything */
 } lws_cpd_result_t;
 
-
 typedef void (*lws_attach_cb_t)(struct lws_context *context, int tsi, void *opaque);
 struct lws_attach_item;
 
@@ -182,6 +181,11 @@ typedef struct lws_system_ops {
 	 * by calling lws_captive_portal_detect_result() api
 	 */
 
+	int (*metric_report)(lws_metric_pub_t *mdata);
+	/**< metric \p item is reporting an event of kind \p rpt,
+	 * held in \p mdata... return 0 to leave the metric object as it is,
+	 * or nonzero to reset it. */
+
 	uint32_t	wake_latency_us;
 	/**< time taken for this device to wake from suspend, in us
 	 */
diff --git a/lib/core-net/CMakeLists.txt b/lib/core-net/CMakeLists.txt
index e401506d83803ad0085eb8976995ac873037a735..96c742ed72dca0f133e0c6cd1376e779a00c1dfa 100644
--- a/lib/core-net/CMakeLists.txt
+++ b/lib/core-net/CMakeLists.txt
@@ -51,11 +51,6 @@ if (LWS_WITH_NETLINK)
 	)
 endif()
 
-if (LWS_WITH_DETAILED_LATENCY)
-	list(APPEND SOURCES
-		core-net/detailed-latency.c)
-endif()
-
 if (LWS_WITH_LWS_DSH)
 	list(APPEND SOURCES
 		core-net/lws-dsh.c)
@@ -77,20 +72,9 @@ if (LWS_WITH_CLIENT)
 	)
 endif()
 
-if (NOT LWS_WITHOUT_SERVER)
-	list(APPEND SOURCES
-		core-net/server.c)
-endif()
-
 if (LWS_WITH_SOCKS5 AND NOT LWS_WITHOUT_CLIENT)
 	list(APPEND SOURCES
 		core-net/socks5-client.c)
 endif()
 
-if (LWS_WITH_NETWORK AND LWS_WITH_STATS)
-	list(APPEND SOURCES
-		core-net/stats.c
-	)
-endif()
-
 exports_to_parent_scope()
diff --git a/lib/core-net/adopt.c b/lib/core-net/adopt.c
index 8c2c05573565c244f183a1236c13aaf1e8b9afe4..98c92d3482e875d5121924251f35a3c5038d9113 100644
--- a/lib/core-net/adopt.c
+++ b/lib/core-net/adopt.c
@@ -66,7 +66,11 @@ lws_create_new_server_wsi(struct lws_vhost *vhost, int fixed_tsi, const char *de
 		return NULL;
 	}
 
-	__lws_lc_tag(&vhost->context->lcg[LWSLCG_WSI_SERVER], &new_wsi->lc, desc);
+	__lws_lc_tag(&vhost->context->lcg[
+#if defined(LWS_ROLE_H2) || defined(LWS_ROLE_MQTT)
+	strcmp(desc, "adopted") ? LWSLCG_WSI_MUX :
+#endif
+	LWSLCG_WSI_SERVER], &new_wsi->lc, desc);
 
 	new_wsi->wsistate |= LWSIFR_SERVER;
 	new_wsi->tsi = (char)n;
@@ -77,11 +81,6 @@ lws_create_new_server_wsi(struct lws_vhost *vhost, int fixed_tsi, const char *de
 	new_wsi->rxflow_change_to = LWS_RXFLOW_ALLOW;
 	new_wsi->retry_policy = vhost->retry_policy;
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (vhost->context->detailed_latency_cb)
-		new_wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
-
 	/* initialize the instance struct */
 
 	lwsi_set_state(new_wsi, LRS_UNCONNECTED);
@@ -145,8 +144,6 @@ lws_adopt_descriptor_vhost1(struct lws_vhost *vh, lws_adoption_type type,
 	pt = &context->pt[(int)new_wsi->tsi];
 	lws_pt_lock(pt, __func__);
 
-	lws_stats_bump(pt, LWSSTATS_C_CONNECTIONS, 1);
-
 	if (parent) {
 		new_wsi->parent = parent;
 		new_wsi->sibling_list = parent->child_list;
@@ -176,6 +173,11 @@ lws_adopt_descriptor_vhost1(struct lws_vhost *vh, lws_adoption_type type,
 		goto bail;
 	}
 
+#if defined(LWS_WITH_SERVER)
+	if (new_wsi->role_ops)
+		lws_metrics_tag_wsi_add(new_wsi, "role", new_wsi->role_ops->name);
+#endif
+
 	lws_pt_unlock(pt);
 
 	/*
@@ -494,9 +496,6 @@ lws_adopt_descriptor_vhost_via_info(const lws_adopt_desc_t *info)
 		    peer->count_wsi >= info->vh->context->ip_limit_wsi) {
 			lwsl_info("Peer reached wsi limit %d\n",
 					info->vh->context->ip_limit_wsi);
-			lws_stats_bump(&info->vh->context->pt[0],
-					      LWSSTATS_C_PEER_LIMIT_WSI_DENIED,
-					      1);
 			if (info->vh->context->pl_notify_cb)
 				info->vh->context->pl_notify_cb(
 							info->vh->context,
diff --git a/lib/core-net/client/connect.c b/lib/core-net/client/connect.c
index dd3ca35168b1e907381694acc4de1c19d48f9702..edc4b9a5b9fd96fc23b2d6a53716e3405e91d8b3 100644
--- a/lib/core-net/client/connect.c
+++ b/lib/core-net/client/connect.c
@@ -86,8 +86,12 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i)
 	struct lws *wsi, *safe = NULL;
 	const struct lws_protocols *p;
 	const char *cisin[CIS_COUNT];
-	int tid = 0, n, tsi = 0;
 	struct lws_vhost *vh;
+	int
+#if LWS_MAX_SMP > 1
+		tid = 0,
+#endif
+		n, tsi = 0;
 	size_t size;
 	char *pc;
 
@@ -105,9 +109,6 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i)
 	if (i->local_protocol_name)
 		local = i->local_protocol_name;
 
-	lws_stats_bump(&i->context->pt[tid], LWSSTATS_C_CONNS_CLIENT, 1);
-
-
 	lws_context_lock(i->context, __func__);
 	/*
 	 * PHASE 1: if SMP, find out the tsi related to current service thread
@@ -161,10 +162,6 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i)
 		lws_fi_import(&wsi->fi, i->fi);
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY) && LWS_MAX_SMP > 1
-	wsi->detlat.tsi = tsi;
-#endif
-
 	/*
 	 * Until we exit, we can report connection failure directly to the
 	 * caller without needing to call through to protocol CONNECTION_ERROR.
@@ -186,11 +183,6 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i)
 	else
 		wsi->retry_policy = &i->context->default_retry;
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (i->context->detailed_latency_cb)
-		wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
-
 	if (i->ssl_connection & LCCSCF_WAKE_SUSPEND__VALIDITY)
 		wsi->conn_validity_wakesuspend = 1;
 
@@ -370,7 +362,8 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i)
 			&wsi->lc, "%s/%s/%s/(%s)", i->method ? i->method : "WS",
 			wsi->role_ops->name, i->address,
 #if defined(LWS_WITH_SECURE_STREAMS_PROXY_API)
-			wsi->client_bound_sspc ? lws_sspc_tag((lws_sspc_handle_t *)i->opaque_user_data) :
+			wsi->client_bound_sspc ?
+				lws_sspc_tag((lws_sspc_handle_t *)i->opaque_user_data) :
 #endif
 			lws_ss_tag(((lws_ss_handle_t *)i->opaque_user_data)));
 	} else
@@ -379,6 +372,8 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i)
 			     "%s/%s/%s", i->method ? i->method : "WS",
 			     wsi->role_ops->name, i->address);
 
+	lws_metrics_tag_wsi_add(wsi, "vh", wsi->a.vhost->name);
+
 	pc = (char *)&wsi->stash[1];
 
 	for (n = 0; n < CIS_COUNT; n++)
@@ -533,7 +528,5 @@ bail2:
 	if (i->pwsi)
 		*i->pwsi = NULL;
 
-	lws_stats_bump(&i->context->pt[tid], LWSSTATS_C_CONNS_CLIENT_FAILED, 1);
-
 	return NULL;
 }
diff --git a/lib/core-net/client/connect2.c b/lib/core-net/client/connect2.c
index 9e1bbe5d4a11a5d95d0bf4663d21ceae94258eac..2e703345403f09f95607a166712cb7c796605f50 100644
--- a/lib/core-net/client/connect2.c
+++ b/lib/core-net/client/connect2.c
@@ -32,7 +32,11 @@
 static int
 lws_getaddrinfo46(struct lws *wsi, const char *ads, struct addrinfo **result)
 {
+	lws_metrics_caliper_declare(cal, wsi->a.context->mt_conn_dns);
 	struct addrinfo hints;
+#if defined(LWS_WITH_SYS_METRICS)
+	char buckname[32];
+#endif
 	int n;
 
 	memset(&hints, 0, sizeof(hints));
@@ -79,12 +83,26 @@ lws_getaddrinfo46(struct lws *wsi, const char *ads, struct addrinfo **result)
 
 #endif
 		wsi->dns_reachability = 1;
-		lwsl_notice("%s: asking to recheck CPD in 1ms\n", __func__);
-		lws_system_cpd_start_defer(wsi->a.context, LWS_US_PER_MS);
+		lws_metrics_caliper_report(cal, METRES_NOGO);
+#if defined(LWS_WITH_SYS_METRICS)
+		lws_snprintf(buckname, sizeof(buckname), "dns=\"unreachable %d\"", n);
+		lws_metrics_hist_bump_priv_wsi(wsi, mth_conn_failures, buckname);
+#endif
+		lwsl_notice("%s: asking to recheck CPD in 1s\n", __func__);
+		lws_system_cpd_start_defer(wsi->a.context, LWS_US_PER_SEC);
 	}
 
 	lwsl_info("%s: getaddrinfo '%s' says %d\n", __func__, ads, n);
 
+#if defined(LWS_WITH_SYS_METRICS)
+	if (n < 0) {
+		lws_snprintf(buckname, sizeof(buckname), "dns=\"nores %d\"", n);
+		lws_metrics_hist_bump_priv_wsi(wsi, mth_conn_failures, buckname);
+	}
+#endif
+
+	lws_metrics_caliper_report(cal, n >= 0 ? METRES_GO : METRES_NOGO);
+
 	return n;
 }
 #endif
@@ -260,19 +278,6 @@ solo:
 	}
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (lwsi_state(wsi) == LRS_WAITING_DNS &&
-	    wsi->a.context->detailed_latency_cb) {
-		wsi->detlat.type = LDLT_NAME_RESOLUTION;
-		wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] =
-			(uint32_t)(lws_now_usecs() -
-			wsi->detlat.earliest_write_req_pre_write);
-		wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-		lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-		wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-	}
-#endif
-
 #if defined(LWS_CLIENT_HTTP_PROXYING) && \
 	(defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2))
 
@@ -313,9 +318,6 @@ solo:
 	lwsl_info("%s: %s: lookup %s:%u\n", __func__, wsi->lc.gutag, ads, port);
 	wsi->conn_port = (uint16_t)port;
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
 #if !defined(LWS_WITH_SYS_ASYNC_DNS)
 	n = 0;
 	if (!wsi->dns_sorted_list.count) {
diff --git a/lib/core-net/client/connect3.c b/lib/core-net/client/connect3.c
index 5418cfd5d3b65ead0dc920f2bf3ef4d261be5b88..ec1b250c3a1d738aa5095918a25586c0ada31167 100644
--- a/lib/core-net/client/connect3.c
+++ b/lib/core-net/client/connect3.c
@@ -203,6 +203,7 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads,
 		default:
 			lwsl_debug("%s: getsockopt check: conn fail: errno %d\n",
 					__func__, LWS_ERRNO);
+			lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO);
 			goto try_next_dns_result_fds;
 		}
 	}
@@ -236,19 +237,6 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads,
 	}
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (lwsi_state(wsi) == LRS_WAITING_DNS &&
-	    wsi->a.context->detailed_latency_cb) {
-		wsi->detlat.type = LDLT_NAME_RESOLUTION;
-		wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] =
-			(uint32_t)(lws_now_usecs() -
-			wsi->detlat.earliest_write_req_pre_write);
-		wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-		lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-		wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-	}
-#endif
-
 	/*
 	 * Let's try directly connecting to each of the results in turn until
 	 * one works, or we run out of results...
@@ -393,11 +381,6 @@ ads_known:
 	 * The actual connection attempt
 	 */
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	wsi->detlat.earliest_write_req =
-		wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
-
 #if defined(LWS_ESP_PLATFORM)
 	errno = 0;
 #endif
@@ -412,6 +395,12 @@ ads_known:
 	 * Finally, make the actual connection attempt
 	 */
 
+#if defined(LWS_WITH_SYS_METRICS)
+	if (wsi->cal_conn.mt)
+		lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO);
+	lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mt_conn_tcp);
+#endif
+
 	m = connect(wsi->desc.sockfd, (const struct sockaddr *)psa, (unsigned int)n);
 	if (m == -1) {
 		/*
@@ -438,6 +427,8 @@ ads_known:
 			 * The connect() failed immediately...
 			 */
 
+			lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO);
+
 #if defined(_DEBUG)
 #if defined(LWS_WITH_UNIX_SOCK)
 			if (!wsi->unix_skt) {
@@ -503,7 +494,12 @@ conn_good:
 				&salen) == -1)
 			lwsl_warn("getsockname: %s\n", strerror(LWS_ERRNO));
 #if defined(_DEBUG)
-		lws_sa46_write_numeric_address(&wsi->sa46_local, buf, sizeof(buf));
+#if defined(LWS_WITH_UNIX_SOCK)
+		if (wsi->unix_skt)
+			buf[0] = '\0';
+		else
+#endif
+			lws_sa46_write_numeric_address(&wsi->sa46_local, buf, sizeof(buf));
 
 		lwsl_info("%s: %s: source ads %s\n", __func__, wsi->lc.gutag, buf);
 #endif
@@ -511,20 +507,7 @@ conn_good:
 #endif
 
 	lws_sul_cancel(&wsi->sul_connect_timeout);
-
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (wsi->a.context->detailed_latency_cb) {
-		wsi->detlat.type = LDLT_CONNECTION;
-		wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] =
-			(uint32_t)(lws_now_usecs() -
-			wsi->detlat.earliest_write_req_pre_write);
-		wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-		lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-		wsi->detlat.earliest_write_req =
-			wsi->detlat.earliest_write_req_pre_write =
-							lws_now_usecs();
-	}
-#endif
+	lws_metrics_caliper_report(wsi->cal_conn, METRES_GO);
 
 	lws_addrinfo_clean(wsi);
 
@@ -550,6 +533,8 @@ oom4:
 		/* do the full wsi close flow */
 		goto failed1;
 
+	lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO);
+
 	/*
 	 * We can't be an active client connection any more, if we thought
 	 * that was what we were going to be doing.  It should be if we are
diff --git a/lib/core-net/client/connect4.c b/lib/core-net/client/connect4.c
index eaa05e2cc4f7e26eae5ec24625fe431f0f4ce263..89611202f2c0fdd2db52ac664e12f17ad29847b2 100644
--- a/lib/core-net/client/connect4.c
+++ b/lib/core-net/client/connect4.c
@@ -156,11 +156,7 @@ send_hs:
 		 * wait in the queue until it's possible to send them.
 		 */
 		lws_callback_on_writable(wsi_piggyback);
-#if defined(LWS_WITH_DETAILED_LATENCY)
-		wsi->detlat.earliest_write_req =
-				wsi->detlat.earliest_write_req_pre_write =
-								lws_now_usecs();
-#endif
+
 		lwsl_info("%s: %s: waiting to send hdrs (par state 0x%x)\n",
 			    __func__, wsi->lc.gutag, lwsi_state(wsi_piggyback));
 	} else {
diff --git a/lib/core-net/close.c b/lib/core-net/close.c
index fe01dff515f2d8e12cfb84f52c057ae6acde14cb..806d772b63c5d4fce9dbb26f108402334b5bb0f9 100644
--- a/lib/core-net/close.c
+++ b/lib/core-net/close.c
@@ -238,8 +238,6 @@ lws_inform_client_conn_fail(struct lws *wsi, void *arg, size_t len)
 		return;
 
 	wsi->already_did_cce = 1;
-	lws_stats_bump(&wsi->a.context->pt[(int)wsi->tsi],
-		       LWSSTATS_C_CONNS_CLIENT_FAILED, 1);
 
 	if (!wsi->a.protocol)
 		return;
@@ -293,6 +291,20 @@ __lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason,
 	context = wsi->a.context;
 	pt = &context->pt[(int)wsi->tsi];
 
+#if defined(LWS_WITH_SYS_METRICS) && \
+    (defined(LWS_WITH_CLIENT) || defined(LWS_WITH_SERVER))
+	/* wsi level: only reports if dangling caliper */
+	if (wsi->cal_conn.mt && wsi->cal_conn.us_start) {
+		if ((lws_metrics_priv_to_pub(wsi->cal_conn.mt)->flags) & LWSMTFL_REPORT_HIST) {
+			lws_metrics_caliper_report_hist(wsi->cal_conn, (struct lws *)NULL);
+		} else {
+			lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO);
+			lws_metrics_caliper_done(wsi->cal_conn);
+		}
+	} else
+		lws_metrics_caliper_done(wsi->cal_conn);
+#endif
+
 #if defined(LWS_WITH_SYS_ASYNC_DNS)
 	if (wsi == context->async_dns.wsi)
 		context->async_dns.wsi = NULL;
@@ -300,8 +312,6 @@ __lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason,
 
 	lws_pt_assert_lock_held(pt);
 
-	lws_stats_bump(pt, LWSSTATS_C_API_CLOSE, 1);
-
 #if defined(LWS_WITH_CLIENT)
 
 	lws_free_set_NULL(wsi->cli_hostname_copy);
@@ -719,6 +729,14 @@ async_close:
 				lws_sspc_handle_t *h = (lws_sspc_handle_t *)wsi->a.opaque_user_data;
 
 				if (h) { // && (h->info.flags & LWSSSINFLAGS_ACCEPTED)) {
+
+#if defined(LWS_WITH_SYS_METRICS)
+					/*
+					 * If any hanging caliper measurement, dump it, and free any tags
+					 */
+					lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
+
 					h->cwsi = NULL;
 					//wsi->a.opaque_user_data = NULL;
 				}
@@ -729,6 +747,12 @@ async_close:
 
 			if (h) { // && (h->info.flags & LWSSSINFLAGS_ACCEPTED)) {
 
+				/*
+				 * ss level: only reports if dangling caliper
+				 * not already reported
+				 */
+				lws_metrics_caliper_report_hist(h->cal_txn, wsi);
+
 				h->wsi = NULL;
 				wsi->a.opaque_user_data = NULL;
 
diff --git a/lib/core-net/detailed-latency.c b/lib/core-net/detailed-latency.c
deleted file mode 100644
index a8c1d09881c099617be83887a821f80222f6399b..0000000000000000000000000000000000000000
--- a/lib/core-net/detailed-latency.c
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * libwebsockets - small server side websockets and web server implementation
- *
- * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to
- * deal in the Software without restriction, including without limitation the
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- */
-
-#include "private-lib-core.h"
-
-int
-lws_det_lat_active(struct lws_context *context)
-{
-	return !!context->detailed_latency_cb;
-}
-
-int
-lws_det_lat_cb(struct lws_context *context, lws_detlat_t *d)
-{
-	int n;
-
-	if (!context->detailed_latency_cb)
-		return 0;
-
-	n = context->detailed_latency_cb(context, d);
-
-	memset(&d->latencies, 0, sizeof(d->latencies));
-
-	return n;
-}
-
-static const char types[] = "rwNCTt????";
-int
-lws_det_lat_plot_cb(struct lws_context *context, const lws_detlat_t *d)
-{
-	char buf[80], *p = buf, *end = &p[sizeof(buf) - 1];
-
-	if (!context->detailed_latency_filepath)
-		return 1;
-
-	if (context->latencies_fd == -1) {
-		context->latencies_fd = open(context->detailed_latency_filepath,
-				LWS_O_CREAT | LWS_O_TRUNC | LWS_O_WRONLY, 0600);
-		if (context->latencies_fd == -1)
-			return 1;
-	}
-
-	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
-			  "%llu %c %u %u %u %u %u %zu %zu\n",
-			  (unsigned long long)lws_now_usecs(), types[d->type],
-			  d->latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE],
-			  d->latencies[LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX],
-			  d->latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE],
-			  d->latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] +
-			  d->latencies[LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX] +
-			  d->latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE],
-			  d->latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX],
-			  d->acc_size, d->req_size);
-
-	write(context->latencies_fd, buf, lws_ptr_diff_size_t(p, buf));
-
-	return 0;
-}
diff --git a/lib/core-net/network.c b/lib/core-net/network.c
index 72d5db99fabef65c0989f04160c6afd00cb05507..866ff3c33dc326d6a45773ee7959d012d1b1c936 100644
--- a/lib/core-net/network.c
+++ b/lib/core-net/network.c
@@ -890,7 +890,7 @@ lws_sa46_write_numeric_address(lws_sockaddr46 *sa46, char *buf, size_t len)
 		return lws_snprintf(buf, len, "(unset)");
 
 	if (sa46->sa4.sin_family == AF_INET6)
-		lws_snprintf(buf, len, "(ipv6 unsupp)");
+		return lws_snprintf(buf, len, "(ipv6 unsupp)");
 
 	lws_snprintf(buf, len, "(AF%d unsupp)", (int)sa46->sa4.sin_family);
 
diff --git a/lib/core-net/output.c b/lib/core-net/output.c
index 71e59122fc560d6c17fc6c57cf49172ef8642eaf..8a2fd901eb0d6b16b30a539dae40a175a4cc141c 100644
--- a/lib/core-net/output.c
+++ b/lib/core-net/output.c
@@ -31,7 +31,6 @@ int
 lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len)
 {
 	struct lws_context *context = lws_get_context(wsi);
-	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];
 	size_t real_len = len;
 	unsigned int n, m;
 
@@ -59,8 +58,6 @@ lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len)
 			  (unsigned long)len);
 	}
 
-	lws_stats_bump(pt, LWSSTATS_C_API_WRITE, 1);
-
 	/* just ignore sends after we cleared the truncation buffer */
 	if (lwsi_state(wsi) == LRS_FLUSHING_BEFORE_CLOSE &&
 	    !lws_has_buffered_out(wsi)
@@ -215,9 +212,6 @@ lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len)
 				       real_len - m) < 0)
 		return -1;
 
-	lws_stats_bump(pt, LWSSTATS_C_WRITE_PARTIALS, 1);
-	lws_stats_bump(pt, LWSSTATS_B_PARTIALS_ACCEPTED_PARTS, m);
-
 #if defined(LWS_WITH_UDP)
 	if (lws_wsi_is_udp(wsi))
 		/* stash original destination for fulfilling UDP partials */
@@ -234,57 +228,30 @@ int
 lws_write(struct lws *wsi, unsigned char *buf, size_t len,
 	  enum lws_write_protocol wp)
 {
-	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	lws_usec_t us;
-#endif
 	int m;
 
-	lws_stats_bump(pt, LWSSTATS_C_API_LWS_WRITE, 1);
-
 	if ((int)len < 0) {
 		lwsl_err("%s: suspicious len int %d, ulong %lu\n", __func__,
 				(int)len, (unsigned long)len);
 		return -1;
 	}
 
-	lws_stats_bump(pt, LWSSTATS_B_WRITE, len);
-
 #ifdef LWS_WITH_ACCESS_LOG
 	wsi->http.access_log.sent += len;
 #endif
-#if defined(LWS_WITH_SERVER_STATUS)
-	if (wsi->a.vhost)
-		wsi->a.vhost->conn_stats.tx += len;
-#endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	us = lws_now_usecs();
-#endif
 
 	assert(wsi->role_ops);
 
 	if (!lws_rops_fidx(wsi->role_ops, LWS_ROPS_write_role_protocol))
-		return lws_issue_raw(wsi, buf, len);
-
-	m = lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_write_role_protocol).
-			write_role_protocol(wsi, buf, len, &wp);
-	if (m < 0)
-		return m;
-
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (wsi->a.context->detailed_latency_cb) {
-		wsi->detlat.req_size = len;
-		wsi->detlat.acc_size = (unsigned int)m;
-		wsi->detlat.type = LDLT_WRITE;
-		if (wsi->detlat.earliest_write_req_pre_write)
-			wsi->detlat.latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE] =
-					(uint32_t)(us - wsi->detlat.earliest_write_req_pre_write);
-		else
-			wsi->detlat.latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE] = 0;
-		wsi->detlat.latencies[LAT_DUR_USERCB] = (uint32_t)(lws_now_usecs() - us);
-		lws_det_lat_cb(wsi->a.context, &wsi->detlat);
+		m = lws_issue_raw(wsi, buf, len);
+	else
+		m = lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_write_role_protocol).
+				write_role_protocol(wsi, buf, len, &wp);
 
-	}
+#if defined(LWS_WITH_SYS_METRICS)
+	if (wsi->a.vhost)
+		lws_metric_event(wsi->a.vhost->mt_traffic_tx, (char)
+				 (m < 0 ? METRES_NOGO : METRES_GO), len);
 #endif
 
 	return m;
@@ -316,19 +283,20 @@ lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, size_t len)
 	en = LWS_ERRNO;
 	if (n >= 0) {
 
-		if (!n && wsi->unix_skt)
-			return LWS_SSL_CAPABLE_ERROR;
+		//if (!n && wsi->unix_skt)
+		//	goto do_err;
 
 		/*
 		 * See https://libwebsockets.org/
 		 * pipermail/libwebsockets/2019-March/007857.html
 		 */
-		if (!n)
-			return LWS_SSL_CAPABLE_ERROR;
+		if (!n && !wsi->unix_skt)
+			goto do_err;
 
-#if defined(LWS_WITH_SERVER_STATUS)
+#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_SERVER)
 		if (wsi->a.vhost)
-			wsi->a.vhost->conn_stats.rx = (unsigned long long)(wsi->a.vhost->conn_stats.rx + (unsigned long long)(long long)n);
+			lws_metric_event(wsi->a.vhost->mt_traffic_rx,
+					 METRES_GO /* rx */, (unsigned int)n);
 #endif
 
 		return n;
@@ -339,7 +307,14 @@ lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, size_t len)
 	    en == LWS_EINTR)
 		return LWS_SSL_CAPABLE_MORE_SERVICE;
 
-	lwsl_info("error on reading from skt : %d\n", en);
+do_err:
+#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_SERVER)
+	if (wsi->a.vhost)
+		lws_metric_event(wsi->a.vhost->mt_traffic_rx, METRES_NOGO, 0u);
+#endif
+
+	lwsl_info("%s: error on reading from skt : %d, errno %d\n",
+			__func__, n, en);
 
 	return LWS_SSL_CAPABLE_ERROR;
 }
diff --git a/lib/core-net/pollfd.c b/lib/core-net/pollfd.c
index f3ecb61cfe89310f5035ccec95e0498f410f956f..a59f86629c6f8de5bf39d5adcbb3cbd8858b2a99 100644
--- a/lib/core-net/pollfd.c
+++ b/lib/core-net/pollfd.c
@@ -515,7 +515,6 @@ lws_change_pollfd(struct lws *wsi, int _and, int _or)
 int
 lws_callback_on_writable(struct lws *wsi)
 {
-	struct lws_context_per_thread *pt;
 	struct lws *w = wsi;
 
 	if (lwsi_state(wsi) == LRS_SHUTDOWN)
@@ -524,21 +523,6 @@ lws_callback_on_writable(struct lws *wsi)
 	if (wsi->socket_is_permanently_unusable)
 		return 0;
 
-	pt = &wsi->a.context->pt[(int)wsi->tsi];
-
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (!wsi->detlat.earliest_write_req)
-		wsi->detlat.earliest_write_req = lws_now_usecs();
-#endif
-
-	lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB_REQ, 1);
-#if defined(LWS_WITH_STATS)
-	if (!wsi->active_writable_req_us) {
-		wsi->active_writable_req_us = lws_now_usecs();
-		lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB_EFF_REQ, 1);
-	}
-#endif
-
 	if (lws_rops_fidx(wsi->role_ops, LWS_ROPS_callback_on_writable)) {
 		int q = lws_rops_func_fidx(wsi->role_ops,
 					   LWS_ROPS_callback_on_writable).
diff --git a/lib/core-net/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h
index a489fc45d142e58191f1dff7dad2e619713fe699..86094fb34107b045d74267a445cb47dc08c2a382 100644
--- a/lib/core-net/private-lib-core-net.h
+++ b/lib/core-net/private-lib-core-net.h
@@ -264,41 +264,6 @@ struct lws_timed_vh_protocol {
 
 #endif
 
-/*
- * lws_dsh
-*/
-
-typedef struct lws_dsh_obj_head {
-	lws_dll2_owner_t		owner;
-	size_t				total_size; /* for this kind in dsh */
-	int				kind;
-} lws_dsh_obj_head_t;
-
-typedef struct lws_dsh_obj {
-	lws_dll2_t			list;	/* must be first */
-	struct lws_dsh	  		*dsh;	/* invalid when on free list */
-	size_t				size;	/* invalid when on free list */
-	size_t				asize;
-	int				kind; /* so we can account at free */
-} lws_dsh_obj_t;
-
-typedef struct lws_dsh {
-	lws_dll2_t			list;
-	uint8_t				*buf;
-	lws_dsh_obj_head_t		*oha;	/* array of object heads/kind */
-	size_t				buffer_size;
-	size_t				locally_in_use;
-	size_t				locally_free;
-	int				count_kinds;
-	uint8_t				being_destroyed;
-	/*
-	 * Overallocations at create:
-	 *
-	 *  - the buffer itself
-	 *  - the object heads array
-	 */
-} lws_dsh_t;
-
 /*
  * lws_async_dns
  */
@@ -371,11 +336,6 @@ struct lws_context_per_thread {
 #if defined(LWS_ROLE_CGI)
 	lws_sorted_usec_list_t sul_cgi;
 #endif
-#if defined(LWS_WITH_STATS)
-	uint64_t lws_stats[LWSSTATS_SIZE];
-	int updated;
-	lws_sorted_usec_list_t sul_stats;
-#endif
 #if defined(LWS_WITH_PEER_LIMITS)
 	lws_sorted_usec_list_t sul_peer_limits;
 #endif
@@ -419,10 +379,6 @@ struct lws_context_per_thread {
 	void		*evlib_pt; /* overallocated */
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	lws_usec_t	ust_left_poll;
-#endif
-
 	/* --- */
 
 	unsigned long count_conns;
@@ -454,14 +410,6 @@ struct lws_context_per_thread {
 	unsigned char is_destroyed:1;
 };
 
-#if defined(LWS_WITH_SERVER_STATUS)
-struct lws_conn_stats {
-	unsigned long long rx, tx;
-	unsigned long h1_conn, h1_trans, h2_trans, ws_upg, h2_alpn, h2_subs,
-		      h2_upg, rejected, mqtt_subs;
-};
-#endif
-
 /*
  * virtual host -related context information
  *   vhostwide SSL context
@@ -510,8 +458,9 @@ struct lws_vhost {
 #if defined(LWS_WITH_EVENT_LIBS)
 	void		*evlib_vh; /* overallocated */
 #endif
-#if defined(LWS_WITH_SERVER_STATUS)
-	struct lws_conn_stats conn_stats;
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metric_t	*mt_traffic_rx;
+	lws_metric_t	*mt_traffic_tx;
 #endif
 
 #if defined(LWS_WITH_SYS_FAULT_INJECTION)
@@ -707,10 +656,6 @@ struct lws {
 	void				*evlib_wsi; /* overallocated */
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	lws_detlat_t	detlat;
-#endif
-
 	lws_sorted_usec_list_t		sul_timeout;
 	lws_sorted_usec_list_t		sul_hrtimer;
 	lws_sorted_usec_list_t		sul_validity;
@@ -728,6 +673,8 @@ struct lws {
 	struct lws_dll2			dll2_cli_txn_queue;
 	struct lws_dll2_owner		dll2_cli_txn_queue_owner;
 
+	/**< caliper is reused for tcp, tls and txn conn phases */
+
 	lws_dll2_t			speculative_list;
 	lws_dll2_owner_t		speculative_connect_owner;
 	/* wsis: additional connection candidates */
@@ -741,6 +688,10 @@ struct lws {
 	/**< Fault Injection ctx for the wsi, hierarchy wsi->vhost->context */
 #endif
 
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metrics_caliper_compose(cal_conn)
+#endif
+
 	lws_sockaddr46			sa46_local;
 	lws_sockaddr46			sa46_peer;
 
@@ -779,12 +730,7 @@ struct lws {
 #endif
 
 	lws_sock_file_fd_type		desc; /* .filefd / .sockfd */
-#if defined(LWS_WITH_STATS)
-	uint64_t active_writable_req_us;
-#if defined(LWS_WITH_TLS)
-	uint64_t accept_start_us;
-#endif
-#endif
+
 	lws_wsi_state_t			wsistate;
 	lws_wsi_state_t			wsistate_pre_close;
 
@@ -909,9 +855,6 @@ struct lws {
 #if defined(LWS_WITH_CGI) || defined(LWS_WITH_CLIENT)
 	char reason_bf; /* internal writeable callback reason bitfield */
 #endif
-#if defined(LWS_WITH_STATS) && defined(LWS_WITH_TLS)
-	char seen_rx;
-#endif
 #if defined(LWS_WITH_NETLINK)
 	lws_route_uidx_t		peer_route_uidx;
 	/**< unique index of the route the connection is estimated to take */
@@ -1224,11 +1167,6 @@ lws_destroy_event_pipe(struct lws *wsi);
 int
 lws_socks5c_generate_msg(struct lws *wsi, enum socks_msg_type type, ssize_t *msg_len);
 
-#if defined(LWS_WITH_SERVER_STATUS)
-void
-lws_sum_stats(const struct lws_context *ctx, struct lws_conn_stats *cs);
-#endif
-
 #if defined(LWS_WITH_DEPRECATED_THINGS)
 int
 __lws_timed_callback_remove(struct lws_vhost *vh, struct lws_timed_vh_protocol *p);
@@ -1433,21 +1371,6 @@ lws_sort_dns(struct lws *wsi, const struct addrinfo *result);
 int
 lws_broadcast(struct lws_context_per_thread *pt, int reason, void *in, size_t len);
 
-#if defined(LWS_WITH_STATS)
- void
- lws_stats_bump(struct lws_context_per_thread *pt, int i, uint64_t bump);
- void
- lws_stats_max(struct lws_context_per_thread *pt, int index, uint64_t val);
-#else
- static LWS_INLINE uint64_t lws_stats_bump(
-		struct lws_context_per_thread *pt, int index, uint64_t bump) {
-	(void)pt; (void)index; (void)bump; return 0; }
- static LWS_INLINE uint64_t lws_stats_max(
-		struct lws_context_per_thread *pt, int index, uint64_t val) {
-	(void)pt; (void)index; (void)val; return 0; }
-#endif
-
-
 
 #if defined(LWS_WITH_PEER_LIMITS)
 void
@@ -1500,6 +1423,9 @@ extern const struct lws_protocols protocol_abs_client_raw_skt,
 void
 __lws_reset_wsi(struct lws *wsi);
 
+void
+lws_metrics_dump(struct lws_context *ctx);
+
 void
 lws_inform_client_conn_fail(struct lws *wsi, void *arg, size_t len);
 
diff --git a/lib/core-net/server.c b/lib/core-net/server.c
deleted file mode 100644
index dff3348498910cae67f8718ea0e15221951e6750..0000000000000000000000000000000000000000
--- a/lib/core-net/server.c
+++ /dev/null
@@ -1,326 +0,0 @@
-/*
- * libwebsockets - small server side websockets and web server implementation
- *
- * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to
- * deal in the Software without restriction, including without limitation the
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- */
-
-#include "private-lib-core.h"
-
-#if defined(LWS_WITH_SERVER_STATUS)
-
-void
-lws_sum_stats(const struct lws_context *ctx, struct lws_conn_stats *cs)
-{
-	const struct lws_vhost *vh = ctx->vhost_list;
-
-	while (vh) {
-
-		cs->rx += vh->conn_stats.rx;
-		cs->tx += vh->conn_stats.tx;
-		cs->h1_conn += vh->conn_stats.h1_conn;
-		cs->h1_trans += vh->conn_stats.h1_trans;
-		cs->h2_trans += vh->conn_stats.h2_trans;
-		cs->ws_upg += vh->conn_stats.ws_upg;
-		cs->h2_upg += vh->conn_stats.h2_upg;
-		cs->h2_alpn += vh->conn_stats.h2_alpn;
-		cs->h2_subs += vh->conn_stats.h2_subs;
-		cs->rejected += vh->conn_stats.rejected;
-
-		vh = vh->vhost_next;
-	}
-}
-
-int
-lws_json_dump_vhost(const struct lws_vhost *vh, char *buf, int len)
-{
-#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
-	static const char * const prots[] = {
-		"http://",
-		"https://",
-		"file://",
-		"cgi://",
-		">http://",
-		">https://",
-		"callback://"
-	};
-#endif
-	char *orig = buf, *end = buf + len - 1, first;
-	int n;
-
-	if (len < 100)
-		return 0;
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-			"{\n \"name\":\"%s\",\n"
-			" \"port\":\"%d\",\n"
-			" \"use_ssl\":\"%d\",\n"
-			" \"sts\":\"%d\",\n"
-			" \"rx\":\"%llu\",\n"
-			" \"tx\":\"%llu\",\n"
-			" \"h1_conn\":\"%lu\",\n"
-			" \"h1_trans\":\"%lu\",\n"
-			" \"h2_trans\":\"%lu\",\n"
-			" \"ws_upg\":\"%lu\",\n"
-			" \"rejected\":\"%lu\",\n"
-			" \"h2_upg\":\"%lu\",\n"
-			" \"h2_alpn\":\"%lu\",\n"
-			" \"h2_subs\":\"%lu\""
-			,
-			vh->name, vh->listen_port,
-#if defined(LWS_WITH_TLS)
-			vh->tls.use_ssl & LCCSCF_USE_SSL,
-#else
-			0,
-#endif
-			!!(vh->options & LWS_SERVER_OPTION_STS),
-			vh->conn_stats.rx, vh->conn_stats.tx,
-			vh->conn_stats.h1_conn,
-			vh->conn_stats.h1_trans,
-			vh->conn_stats.h2_trans,
-			vh->conn_stats.ws_upg,
-			vh->conn_stats.rejected,
-			vh->conn_stats.h2_upg,
-			vh->conn_stats.h2_alpn,
-			vh->conn_stats.h2_subs
-	);
-#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
-	if (vh->http.mount_list) {
-		const struct lws_http_mount *m = vh->http.mount_list;
-
-		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",\n \"mounts\":[");
-		first = 1;
-		while (m) {
-			if (!first)
-				buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",");
-			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-					"\n  {\n   \"mountpoint\":\"%s\",\n"
-					"  \"origin\":\"%s%s\",\n"
-					"  \"cache_max_age\":\"%d\",\n"
-					"  \"cache_reuse\":\"%d\",\n"
-					"  \"cache_revalidate\":\"%d\",\n"
-					"  \"cache_intermediaries\":\"%d\"\n"
-					,
-					m->mountpoint,
-					prots[m->origin_protocol],
-					m->origin,
-					m->cache_max_age,
-					m->cache_reusable,
-					m->cache_revalidate,
-					m->cache_intermediaries);
-			if (m->def)
-				buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-						",\n  \"default\":\"%s\"",
-						m->def);
-			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n  }");
-			first = 0;
-			m = m->mount_next;
-		}
-		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n ]");
-	}
-#endif
-	if (vh->protocols) {
-		n = 0;
-		first = 1;
-
-		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",\n \"ws-protocols\":[");
-		while (n < vh->count_protocols) {
-			if (!first)
-				buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",");
-			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-					"\n  {\n   \"%s\":{\n"
-					"    \"status\":\"ok\"\n   }\n  }"
-					,
-					vh->protocols[n].name);
-			first = 0;
-			n++;
-		}
-		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n ]");
-	}
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n}");
-
-	return lws_ptr_diff(buf, orig);
-}
-
-
-int
-lws_json_dump_context(const struct lws_context *context, char *buf, int len,
-		int hide_vhosts)
-{
-	char *orig = buf, *end = buf + len - 1, first = 1;
-	const struct lws_vhost *vh = context->vhost_list;
-	const struct lws_context_per_thread *pt;
-	int n, listening = 0, cgi_count = 0, fd;
-	struct lws_conn_stats cs;
-	double d = 0;
-#ifdef LWS_WITH_CGI
-	struct lws_cgi * const *pcgi;
-#endif
-
-//#ifdef LWS_WITH_LIBUV &&
-//	uv_uptime(&d);
-//#endif
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "{ "
-			    "\"version\":\"%s\",\n"
-			    "\"uptime\":\"%ld\",\n",
-			    lws_get_library_version(),
-			    (long)d);
-
-#ifdef LWS_HAVE_GETLOADAVG
-#if defined(__sun)
-#include <sys/loadavg.h>
-#endif
-	{
-		double d[3];
-		int m;
-
-		m = getloadavg(d, 3);
-		for (n = 0; n < m; n++) {
-			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-				"\"l%d\":\"%.2f\",\n",
-				n + 1, d[n]);
-		}
-	}
-#endif
-
-	fd = lws_open("/proc/self/statm", LWS_O_RDONLY);
-	if (fd >= 0) {
-		char contents[96], pure[96];
-		n = (int)read(fd, contents, sizeof(contents) - 1);
-		if (n > 0) {
-			contents[n] = '\0';
-			if (contents[n - 1] == '\n')
-				contents[--n] = '\0';
-			lws_json_purify(pure, contents, sizeof(pure), NULL);
-
-			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-					  "\"statm\": \"%s\",\n", pure);
-		}
-		close(fd);
-	}
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\"heap\":%lld,\n\"contexts\":[\n",
-				(long long)lws_get_allocated_heap());
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "{ "
-				"\"context_uptime\":\"%llu\",\n"
-				"\"cgi_spawned\":\"%d\",\n"
-				"\"pt_fd_max\":\"%d\",\n"
-				"\"ah_pool_max\":\"%d\",\n"
-				"\"deprecated\":\"%d\",\n"
-				"\"wsi_alive\":\"",
-				(unsigned long long)(lws_now_usecs() - context->time_up) /
-					LWS_US_PER_SEC,
-				context->count_cgi_spawned,
-				context->fd_limit_per_thread,
-				context->max_http_header_pool,
-				context->deprecated);
-
-	for (n = 0; n < LWSLCG_COUNT; n++)
-		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%u ",
-				context->lcg[n].owner.count);
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\", \"pt\":[\n ");
-	for (n = 0; n < context->count_threads; n++) {
-		pt = &context->pt[n];
-		if (n)
-			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",");
-		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-				"\n  {\n"
-				"    \"fds_count\":\"%d\",\n"
-				"    \"ah_pool_inuse\":\"%d\",\n"
-				"    \"ah_wait_list\":\"%d\"\n"
-				"    }",
-				pt->fds_count,
-				pt->http.ah_count_in_use,
-				pt->http.ah_wait_list_length);
-	}
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "]");
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", \"vhosts\":[\n ");
-
-	first = 1;
-	vh = context->vhost_list;
-	listening = 0;
-	cs = context->conn_stats;
-	lws_sum_stats(context, &cs);
-	while (vh) {
-
-		if (!hide_vhosts) {
-			if (!first)
-				if (buf != end)
-					*buf++ = ',';
-			buf += lws_json_dump_vhost(vh, buf, lws_ptr_diff(end, buf));
-			first = 0;
-		}
-		if (vh->lserv_wsi)
-			listening++;
-		vh = vh->vhost_next;
-	}
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
-			"],\n\"listen_wsi\":\"%d\",\n"
-			" \"rx\":\"%llu\",\n"
-			" \"tx\":\"%llu\",\n"
-			" \"h1_conn\":\"%lu\",\n"
-			" \"h1_trans\":\"%lu\",\n"
-			" \"h2_trans\":\"%lu\",\n"
-			" \"ws_upg\":\"%lu\",\n"
-			" \"rejected\":\"%lu\",\n"
-			" \"h2_alpn\":\"%lu\",\n"
-			" \"h2_subs\":\"%lu\",\n"
-			" \"h2_upg\":\"%lu\"",
-			listening, cs.rx, cs.tx,
-			cs.h1_conn,
-			cs.h1_trans,
-			cs.h2_trans,
-			cs.ws_upg,
-			cs.rejected,
-			cs.h2_alpn,
-			cs.h2_subs,
-			cs.h2_upg);
-
-#ifdef LWS_WITH_CGI
-	for (n = 0; n < context->count_threads; n++) {
-		pt = &context->pt[n];
-		pcgi = &pt->http.cgi_list;
-
-		while (*pcgi) {
-			pcgi = &(*pcgi)->cgi_list;
-
-			cgi_count++;
-		}
-	}
-#endif
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",\n \"cgi_alive\":\"%d\"\n ",
-			cgi_count);
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "}");
-
-
-	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "]}\n ");
-
-	return lws_ptr_diff(buf, orig);
-}
-
-#endif
diff --git a/lib/core-net/service.c b/lib/core-net/service.c
index 62a73a4a1d1e86098405972ef3c070f3ab998d4d..5e0370521604d4db8b5c084ee411d44901d8c8d8 100644
--- a/lib/core-net/service.c
+++ b/lib/core-net/service.c
@@ -27,31 +27,8 @@
 int
 lws_callback_as_writeable(struct lws *wsi)
 {
-	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];
 	int n, m;
 
-	lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB, 1);
-#if defined(LWS_WITH_STATS)
-	if (wsi->active_writable_req_us) {
-		uint64_t ul = lws_now_usecs() -
-			      wsi->active_writable_req_us;
-
-		lws_stats_bump(pt, LWSSTATS_US_WRITABLE_DELAY_AVG, ul);
-		lws_stats_max(pt, LWSSTATS_US_WORST_WRITABLE_DELAY, ul);
-		wsi->active_writable_req_us = 0;
-	}
-#endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (wsi->a.context->detailed_latency_cb && lwsi_state_est(wsi)) {
-		lws_usec_t us = lws_now_usecs();
-
-		wsi->detlat.earliest_write_req_pre_write =
-					wsi->detlat.earliest_write_req;
-		wsi->detlat.earliest_write_req = 0;
-		wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] =
-		      (uint32_t)(us - wsi->detlat.earliest_write_req_pre_write);
-	}
-#endif
 	n = wsi->role_ops->writeable_cb[lwsi_role_server(wsi)];
 	m = user_callback_handle_rxflow(wsi->a.protocol->callback,
 					wsi, (enum lws_callback_reasons) n,
@@ -827,7 +804,8 @@ lws_service(struct lws_context *context, int timeout_ms)
 	}
 	n = lws_plat_service(context, timeout_ms);
 
-	pt->inside_service = 0;
+	if (n != -1)
+		pt->inside_service = 0;
 
 	return n;
 }
diff --git a/lib/core-net/sorted-usec-list.c b/lib/core-net/sorted-usec-list.c
index 4b312212260418e0c061d8643085704b7076e022..16ff617f107e023610f086127d5cce95506be0ed 100644
--- a/lib/core-net/sorted-usec-list.c
+++ b/lib/core-net/sorted-usec-list.c
@@ -147,6 +147,8 @@ __lws_sul_service_ripe(lws_dll2_owner_t *own, int own_len, lws_usec_t usnow)
 		lws_dll2_remove(&hit->list);
 		hit->us = 0;
 
+		// lwsl_notice("%s: sul: %p\n", __func__, hit->cb);
+
 		pt->inside_lws_service = 1;
 		hit->cb(hit);
 		pt->inside_lws_service = 0;
diff --git a/lib/core-net/stats.c b/lib/core-net/stats.c
deleted file mode 100644
index 0a7e7b6b3e42b4abbece972a955135d4362ceac5..0000000000000000000000000000000000000000
--- a/lib/core-net/stats.c
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * libwebsockets - small server side websockets and web server implementation
- *
- * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to
- * deal in the Software without restriction, including without limitation the
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- */
-
-#include "private-lib-core.h"
-
-#if defined(LWS_WITH_STATS)
-
-uint64_t
-lws_stats_get(struct lws_context *context, int index)
-{
-	struct lws_context_per_thread *pt = &context->pt[0];
-
-	if (index >= LWSSTATS_SIZE)
-		return 0;
-
-	return pt->lws_stats[index];
-}
-
-static const char * stat_names[] = {
-	"C_CONNECTIONS",
-	"C_API_CLOSE",
-	"C_API_READ",
-	"C_API_LWS_WRITE",
-	"C_API_WRITE",
-	"C_WRITE_PARTIALS",
-	"C_WRITEABLE_CB_REQ",
-	"C_WRITEABLE_CB_EFF_REQ",
-	"C_WRITEABLE_CB",
-	"C_SSL_CONNECTIONS_FAILED",
-	"C_SSL_CONNECTIONS_ACCEPTED",
-	"C_SSL_CONNECTIONS_ACCEPT_SPIN",
-	"C_SSL_CONNS_HAD_RX",
-	"C_TIMEOUTS",
-	"C_SERVICE_ENTRY",
-	"B_READ",
-	"B_WRITE",
-	"B_PARTIALS_ACCEPTED_PARTS",
-	"US_SSL_ACCEPT_LATENCY_AVG",
-	"US_WRITABLE_DELAY_AVG",
-	"US_WORST_WRITABLE_DELAY",
-	"US_SSL_RX_DELAY_AVG",
-	"C_PEER_LIMIT_AH_DENIED",
-	"C_PEER_LIMIT_WSI_DENIED",
-	"C_CONNECTIONS_CLIENT",
-	"C_CONNECTIONS_CLIENT_FAILED",
-};
-
-static int
-quantify(struct lws_context *context, int tsi, char *p, int len, int idx,
-	 uint64_t *sum)
-{
-	const lws_humanize_unit_t *schema = humanize_schema_si;
-	struct lws_context_per_thread *pt = &context->pt[tsi];
-	uint64_t u, u1;
-
-	lws_pt_stats_lock(pt);
-	u = pt->lws_stats[idx];
-
-	/* it's supposed to be an average? */
-
-	switch (idx) {
-	case LWSSTATS_US_SSL_ACCEPT_LATENCY_AVG:
-		u1 = pt->lws_stats[LWSSTATS_C_SSL_CONNECTIONS_ACCEPTED];
-		if (u1)
-			u = u / u1;
-		break;
-	case LWSSTATS_US_SSL_RX_DELAY_AVG:
-		u1 = pt->lws_stats[LWSSTATS_C_SSL_CONNS_HAD_RX];
-		if (u1)
-			u = u / u1;
-		break;
-	case LWSSTATS_US_WRITABLE_DELAY_AVG:
-		u1 = pt->lws_stats[LWSSTATS_C_WRITEABLE_CB];
-		if (u1)
-			u = u / u1;
-		break;
-	}
-	lws_pt_stats_unlock(pt);
-
-	*sum += u;
-
-	switch (stat_names[idx][0]) {
-	case 'U':
-		schema = humanize_schema_us;
-		break;
-	case 'B':
-		schema = humanize_schema_si_bytes;
-		break;
-	}
-
-	return lws_humanize(p, len, u, schema);
-}
-
-
-void
-lws_stats_log_dump(struct lws_context *context)
-{
-	struct lws_vhost *v = context->vhost_list;
-	uint64_t summary[LWSSTATS_SIZE];
-	char bufline[128], *p, *end = bufline + sizeof(bufline) - 1;
-	int n, m;
-
-	if (!context->updated)
-		return;
-
-	context->updated = 0;
-	memset(summary, 0, sizeof(summary));
-
-	lwsl_notice("\n");
-	lwsl_notice("LWS internal statistics dump ----->\n");
-	for (n = 0; n < (int)LWS_ARRAY_SIZE(stat_names); n++) {
-		uint64_t u = 0;
-
-		/* if it's all zeroes, don't report it */
-
-		for (m = 0; m < context->count_threads; m++) {
-			struct lws_context_per_thread *pt = &context->pt[m];
-
-			u |= pt->lws_stats[n];
-		}
-		if (!u)
-			continue;
-
-		p = bufline;
-		p += lws_snprintf(p, lws_ptr_diff(end, p), "%28s: ",
-				  stat_names[n]);
-
-		for (m = 0; m < context->count_threads; m++)
-			quantify(context, m, p, lws_ptr_diff(end, p), n, &summary[n]);
-
-		lwsl_notice("%s\n", bufline);
-	}
-
-	lwsl_notice("Simultaneous SSL restriction:  %8d/%d\n",
-			context->simultaneous_ssl,
-			context->simultaneous_ssl_restriction);
-
-	while (v) {
-		if (v->lserv_wsi &&
-		    v->lserv_wsi->position_in_fds_table != LWS_NO_FDS_POS) {
-
-			struct lws_context_per_thread *pt =
-					&context->pt[(int)v->lserv_wsi->tsi];
-			struct lws_pollfd *pfd;
-
-			pfd = &pt->fds[v->lserv_wsi->position_in_fds_table];
-
-			lwsl_notice("  Listen port %d actual POLLIN:   %d\n",
-				    v->listen_port,
-				    (int)pfd->events & LWS_POLLIN);
-		}
-
-		v = v->vhost_next;
-	}
-
-	for (n = 0; n < context->count_threads; n++) {
-		struct lws_context_per_thread *pt = &context->pt[n];
-		struct lws *wl;
-		int m = 0;
-
-		lwsl_notice("PT %d\n", n + 1);
-
-		lws_pt_lock(pt, __func__);
-
-		lwsl_notice("  AH in use / max:                  %d / %d\n",
-				pt->http.ah_count_in_use,
-				context->max_http_header_pool);
-
-		wl = pt->http.ah_wait_list;
-		while (wl) {
-			m++;
-			wl = wl->http.ah_wait_list;
-		}
-
-		lwsl_notice("  AH wait list count / actual:      %d / %d\n",
-				pt->http.ah_wait_list_length, m);
-
-		lws_pt_unlock(pt);
-	}
-
-#if defined(LWS_WITH_PEER_LIMITS)
-	m = 0;
-	for (n = 0; n < (int)context->pl_hash_elements; n++) {
-		lws_start_foreach_llp(struct lws_peer **, peer,
-				      context->pl_hash_table[n]) {
-			m++;
-		} lws_end_foreach_llp(peer, next);
-	}
-
-	lwsl_notice(" Peers: total active %d\n", m);
-	if (m > 10) {
-		m = 10;
-		lwsl_notice("  (showing 10 peers only)\n");
-	}
-
-	if (m) {
-		for (n = 0; n < (int)context->pl_hash_elements; n++) {
-			char buf[72];
-
-			lws_start_foreach_llp(struct lws_peer **, peer,
-					      context->pl_hash_table[n]) {
-				struct lws_peer *df = *peer;
-
-				if (!lws_plat_inet_ntop(df->af, df->addr, buf,
-							sizeof(buf) - 1))
-					strcpy(buf, "unknown");
-#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
-				lwsl_notice("  peer %s: count wsi: %d, count ah: %d\n",
-					    buf, df->count_wsi,
-					    df->http.count_ah);
-#else
-				lwsl_notice("  peer %s: count wsi: %d\n",
-					    buf, df->count_wsi);
-#endif
-
-				if (!--m)
-					break;
-			} lws_end_foreach_llp(peer, next);
-		}
-	}
-#endif
-
-	lwsl_notice("\n");
-}
-
-void
-lws_stats_bump(struct lws_context_per_thread *pt, int i, uint64_t bump)
-{
-	lws_pt_stats_lock(pt);
-	pt->lws_stats[i] += bump;
-	if (i != LWSSTATS_C_SERVICE_ENTRY) {
-		pt->updated = 1;
-		pt->context->updated = 1;
-	}
-	lws_pt_stats_unlock(pt);
-}
-
-void
-lws_stats_max(struct lws_context_per_thread *pt, int index, uint64_t val)
-{
-	lws_pt_stats_lock(pt);
-	if (val > pt->lws_stats[index]) {
-		pt->lws_stats[index] = val;
-		pt->updated = 1;
-		pt->context->updated = 1;
-	}
-	lws_pt_stats_unlock(pt);
-}
-
-#endif
-
-
diff --git a/lib/core-net/vhost.c b/lib/core-net/vhost.c
index a2ce30b5e5582656c416f7756d98030317678105..144b6eaa1fcd64fa7d92a5a50dce4808b8d15e2b 100644
--- a/lib/core-net/vhost.c
+++ b/lib/core-net/vhost.c
@@ -1,7 +1,7 @@
 /*
  * libwebsockets - small server side websockets and web server implementation
  *
- * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
+ * Copyright (C) 2010 - 2021 Andy Green <andy@warmcat.com>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
@@ -119,9 +119,13 @@ lws_role_call_alpn_negotiated(struct lws *wsi, const char *alpn)
 
 	LWS_FOR_EVERY_AVAILABLE_ROLE_START(ar)
 		if (ar->alpn && !strcmp(ar->alpn, alpn) &&
-		    lws_rops_fidx(ar, LWS_ROPS_alpn_negotiated))
+		    lws_rops_fidx(ar, LWS_ROPS_alpn_negotiated)) {
+#if defined(LWS_WITH_SERVER)
+			lws_metrics_tag_wsi_add(wsi, "upg", ar->name);
+#endif
 			return (lws_rops_func_fidx(ar, LWS_ROPS_alpn_negotiated)).
 						   alpn_negotiated(wsi, alpn);
+		}
 	LWS_FOR_EVERY_AVAILABLE_ROLE_END;
 #endif
 	return 0;
@@ -305,6 +309,8 @@ lws_vhd_find_by_pvo(struct lws_context *cx, const char *protname,
 	vh = cx->vhost_list;
 	while (vh) {
 
+		if (vh->protocol_vh_privs) {
+
 		for (n = 0; n < vh->count_protocols; n++) {
 			const struct lws_protocol_vhost_options *pv;
 
@@ -313,7 +319,11 @@ lws_vhd_find_by_pvo(struct lws_context *cx, const char *protname,
 
 			/* this vh has an instance of the required protocol */
 
-			pv = lws_pvo_search(vh->pvo, pvo_name);
+			pv = lws_pvo_search(vh->pvo, protname);
+			if (!pv)
+				continue;
+
+			pv = lws_pvo_search(pv->options, pvo_name);
 			if (!pv)
 				continue;
 
@@ -326,6 +336,8 @@ lws_vhd_find_by_pvo(struct lws_context *cx, const char *protname,
 				 */
 				return vh->protocol_vh_privs[n];
 		}
+		} else
+			lwsl_notice("%s: no privs yet on %s\n", __func__, lws_vh_tag(vh));
 		vh = vh->vhost_next;
 	}
 
@@ -460,7 +472,7 @@ lws_protocol_init(struct lws_context *context)
 
 	context->doing_protocol_init = 1;
 
-	lwsl_notice("%s\n", __func__);
+	lwsl_info("%s\n", __func__);
 
 	while (vh) {
 
@@ -538,10 +550,7 @@ lws_create_vhost(struct lws_context *context,
 	struct lws_protocols *lwsp;
 	int m, f = !info->pvo, fx = 0, abs_pcol_count = 0, sec_pcol_count = 0;
 	char buf[96];
-#if defined(LWS_CLIENT_HTTP_PROXYING) && defined(LWS_WITH_CLIENT) \
-	&& defined(LWS_HAVE_GETENV)
 	char *p;
-#endif
 #if defined(LWS_WITH_SYS_ASYNC_DNS)
 	extern struct lws_protocols lws_async_dns_protocol;
 #endif
@@ -572,6 +581,19 @@ lws_create_vhost(struct lws_context *context,
 		vh->name = "default";
 	else
 		vh->name = info->vhost_name;
+	{
+		char *end = buf + sizeof(buf) - 1;
+		p = buf;
+
+		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "%s", vh->name);
+		if (info->iface)
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "|%s", info->iface);
+		if (info->port && !(info->port & 0xffff))
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "|%u", info->port);
+	}
+
+	__lws_lc_tag(&context->lcg[LWSLCG_VHOST], &vh->lc, "%s|%s|%d", buf,
+			info->iface ? info->iface : "", info->port);
 
 #if defined(LWS_WITH_SYS_FAULT_INJECTION)
 	vh->fi.name = "vh";
@@ -584,9 +606,6 @@ lws_create_vhost(struct lws_context *context,
 		lws_fi_import(&vh->fi, info->fi);
 #endif
 
-	__lws_lc_tag(&context->lcg[LWSLCG_VHOST], &vh->lc, "%s|%s|%d", vh->name,
-			info->iface ? info->iface : "", info->port);
-
 #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
 	vh->http.error_document_404 = info->error_document_404;
 #endif
@@ -804,6 +823,23 @@ lws_create_vhost(struct lws_context *context,
 	vh->http.mount_list = info->mounts;
 #endif
 
+#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_SERVER)
+	{
+		char *end = buf + sizeof(buf) - 1;
+		p = buf;
+
+		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "vh.%s", vh->name);
+		if (info->iface)
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ".%s", info->iface);
+		if (info->port && !(info->port & 0xffff))
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ".%u", info->port);
+		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ".rx");
+		vh->mt_traffic_rx = lws_metric_create(context, 0, buf);
+		p[-2] = 't';
+		vh->mt_traffic_tx = lws_metric_create(context, 0, buf);
+	}
+#endif
+
 #ifdef LWS_WITH_UNIX_SOCK
 	if (LWS_UNIX_SOCK_ENABLED(vh)) {
 		lwsl_info("Creating Vhost '%s' path \"%s\", %d protocols\n",
@@ -909,7 +945,7 @@ lws_create_vhost(struct lws_context *context,
 		goto bail1;
 	}
 #if defined(LWS_WITH_SERVER)
-	lws_context_lock(context, "create_vhost");
+	lws_context_lock(context, __func__);
 	n = _lws_vhost_init_server(info, vh);
 	lws_context_unlock(context);
 	if (n < 0) {
@@ -1397,6 +1433,11 @@ __lws_vhost_destroy2(struct lws_vhost *vh)
 	lws_dll2_foreach_safe(&vh->abstract_instances_owner, NULL, destroy_ais);
 #endif
 
+#if defined(LWS_WITH_SERVER) && defined(LWS_WITH_SYS_METRICS)
+	lws_metric_destroy(&vh->mt_traffic_rx, 0);
+	lws_metric_destroy(&vh->mt_traffic_tx, 0);
+#endif
+
 	lws_dll2_remove(&vh->vh_being_destroyed_list);
 
 #if defined(LWS_WITH_SYS_FAULT_INJECTION)
@@ -1404,6 +1445,7 @@ __lws_vhost_destroy2(struct lws_vhost *vh)
 #endif
 
 	__lws_lc_untag(&vh->lc);
+
 	memset(vh, 0, sizeof(*vh));
 	lws_free(vh);
 }
diff --git a/lib/core-net/wsi-timeout.c b/lib/core-net/wsi-timeout.c
index a51a34ec99d3658bf9a8d11ee860e25cbdf22031..70ef6d88ea10eb2c9b534af810d5a6aab6ba81bb 100644
--- a/lib/core-net/wsi-timeout.c
+++ b/lib/core-net/wsi-timeout.c
@@ -81,9 +81,6 @@ lws_sul_wsitimeout_cb(lws_sorted_usec_list_t *sul)
 	struct lws *wsi = lws_container_of(sul, struct lws, sul_timeout);
 	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];
 
-	if (wsi->pending_timeout != PENDING_TIMEOUT_USER_OK)
-		lws_stats_bump(pt, LWSSTATS_C_TIMEOUTS, 1);
-
 	/* no need to log normal idle keepalive timeout */
 //		if (wsi->pending_timeout != PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE)
 #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
diff --git a/lib/core-net/wsi.c b/lib/core-net/wsi.c
index 4bad83da86fc3bffa6b109fad668075dc94fb3bd..5392ef71a0fafec9786939f4f84072cc10e78dcf 100644
--- a/lib/core-net/wsi.c
+++ b/lib/core-net/wsi.c
@@ -342,9 +342,9 @@ lws_rx_flow_control(struct lws *wsi, int _enable)
 
 	/* any bit set in rxflow_bitmap DISABLEs rxflow control */
 	if (en & LWS_RXFLOW_REASON_APPLIES_ENABLE_BIT)
-		wsi->rxflow_bitmap &= (uint8_t)~(en & 0xff);
+		wsi->rxflow_bitmap = (uint8_t)(wsi->rxflow_bitmap & ~(en & 0xff));
 	else
-		wsi->rxflow_bitmap |= (uint8_t)(en & 0xff);
+		wsi->rxflow_bitmap = (uint8_t)(wsi->rxflow_bitmap | (en & 0xff));
 
 	if ((LWS_RXFLOW_PENDING_CHANGE | (!wsi->rxflow_bitmap)) ==
 	    wsi->rxflow_change_to)
diff --git a/lib/core/context.c b/lib/core/context.c
index e556953ea410867036095adac09d2bda30d3803c..1fddce673262f011d4963b6acd0e664a2f142d22 100644
--- a/lib/core/context.c
+++ b/lib/core/context.c
@@ -53,20 +53,6 @@ lws_get_library_version(void)
 	return library_version;
 }
 
-#if defined(LWS_WITH_STATS)
-static void
-lws_sul_stats_cb(lws_sorted_usec_list_t *sul)
-{
-	struct lws_context_per_thread *pt = lws_container_of(sul,
-			struct lws_context_per_thread, sul_stats);
-
-	lws_stats_log_dump(pt->context);
-
-	__lws_sul_insert_us(&pt->pt_sul_owner[LWSSULLI_MISS_IF_SUSPENDED],
-			    &pt->sul_stats, 10 * LWS_US_PER_SEC);
-}
-#endif
-
 #if defined(LWS_WITH_NETWORK)
 
 #if defined(LWS_WITH_SYS_STATE)
@@ -379,10 +365,11 @@ lws_create_context(const struct lws_context_creation_info *info)
 	pid_t pid_daemon = get_daemonize_pid();
 #endif
 #if defined(LWS_WITH_NETWORK)
+	const lws_plugin_evlib_t *plev = NULL;
 	unsigned short count_threads = 1;
 	uint8_t *u;
 	uint16_t us_wait_resolution = 0;
-#endif
+
 #if defined(__ANDROID__)
 	struct rlimit rt;
 #endif
@@ -394,9 +381,10 @@ lws_create_context(const struct lws_context_creation_info *info)
 		s1 = 4096,
 #endif
 		size = sizeof(struct lws_context);
+#endif
+
 	int n;
 	unsigned int lpf = info->fd_limit_per_thread;
-	const lws_plugin_evlib_t *plev = NULL;
 #if defined(LWS_WITH_EVLIB_PLUGINS) && defined(LWS_WITH_EVENT_LIBS)
 	struct lws_plugin		*evlib_plugin_list = NULL;
 #if defined(_DEBUG)
@@ -429,10 +417,6 @@ lws_create_context(const struct lws_context_creation_info *info)
 		s = "IPV6-off";
 #endif
 
-#if defined(LWS_WITH_STATS)
-	lwsl_info(" LWS_WITH_STATS        : on\n");
-#endif
-
 	lwsl_notice("%s%s\n", opts_str, s);
 
 	if (lws_plat_context_early_init())
@@ -453,7 +437,6 @@ lws_create_context(const struct lws_context_creation_info *info)
 #if !defined(LWS_PLAT_FREERTOS)
 	size += (count_threads * sizeof(struct lws));
 #endif
-#endif /* network */
 
 #if defined(LWS_WITH_POLL)
 	{
@@ -655,6 +638,10 @@ lws_create_context(const struct lws_context_creation_info *info)
 	context->lcg[LWSLCG_VHOST].tag_prefix = "vh";
 	context->lcg[LWSLCG_WSI_SERVER].tag_prefix = "wsisrv"; /* adopted */
 
+#if defined(LWS_ROLE_H2) || defined(LWS_ROLE_MQTT)
+	context->lcg[LWSLCG_WSI_MUX].tag_prefix = "mux", /* a mux child wsi */
+#endif
+
 #if defined(LWS_WITH_CLIENT)
 	context->lcg[LWSLCG_WSI_CLIENT].tag_prefix = "wsicli";
 #endif
@@ -675,6 +662,77 @@ lws_create_context(const struct lws_context_creation_info *info)
 #endif
 #endif
 
+#if defined(LWS_WITH_SYS_METRICS)
+	/*
+	 * If we're not using secure streams, we can still pass in a linked-
+	 * list of metrics policies
+	 */
+	context->metrics_policies = info->metrics_policies;
+	context->metrics_prefix = info->metrics_prefix;
+
+	context->mt_service = lws_metric_create(context,
+					LWSMTFL_REPORT_DUTY_WALLCLOCK_US |
+					LWSMTFL_REPORT_ONLY_GO, "cpu.svc");
+
+#if defined(LWS_WITH_CLIENT)
+
+	context->mt_conn_dns = lws_metric_create(context,
+						 LWSMTFL_REPORT_MEAN |
+						 LWSMTFL_REPORT_DUTY_WALLCLOCK_US,
+						 "n.cn.dns");
+	context->mt_conn_tcp = lws_metric_create(context,
+						 LWSMTFL_REPORT_MEAN |
+						 LWSMTFL_REPORT_DUTY_WALLCLOCK_US,
+						 "n.cn.tcp");
+	context->mt_conn_tls = lws_metric_create(context,
+						 LWSMTFL_REPORT_MEAN |
+						 LWSMTFL_REPORT_DUTY_WALLCLOCK_US,
+						 "n.cn.tls");
+#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
+	context->mt_http_txn = lws_metric_create(context,
+						 LWSMTFL_REPORT_MEAN |
+						 LWSMTFL_REPORT_DUTY_WALLCLOCK_US,
+						 "n.http.txn");
+#endif
+
+	context->mth_conn_failures = lws_metric_create(context,
+					LWSMTFL_REPORT_HIST, "n.cn.failures");
+
+#if defined(LWS_WITH_SYS_ASYNC_DNS)
+	context->mt_adns_cache = lws_metric_create(context,
+						   LWSMTFL_REPORT_MEAN |
+						   LWSMTFL_REPORT_DUTY_WALLCLOCK_US,
+						   "n.cn.adns");
+#endif
+#if defined(LWS_WITH_SECURE_STREAMS)
+	context->mth_ss_conn = lws_metric_create(context, LWSMTFL_REPORT_HIST,
+						 "n.ss.conn");
+#endif
+#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API)
+	context->mt_ss_cliprox_conn = lws_metric_create(context,
+			LWSMTFL_REPORT_HIST,
+							"n.ss.cliprox.conn");
+	context->mt_ss_cliprox_paylat = lws_metric_create(context,
+							  LWSMTFL_REPORT_MEAN |
+							  LWSMTFL_REPORT_DUTY_WALLCLOCK_US,
+							  "n.ss.cliprox.paylat");
+	context->mt_ss_proxcli_paylat = lws_metric_create(context,
+							  LWSMTFL_REPORT_MEAN |
+							  LWSMTFL_REPORT_DUTY_WALLCLOCK_US,
+							  "n.ss.proxcli.paylat");
+#endif
+
+#endif /* network + metrics + client */
+
+#if defined(LWS_WITH_SERVER)
+	context->mth_srv = lws_metric_create(context,
+					     LWSMTFL_REPORT_HIST, "n.srv");
+#endif /* network + metrics + server */
+
+#endif /* network + metrics */
+
+#endif /* network */
+
 	/*
 	 * Proxy group
 	 */
@@ -713,11 +771,7 @@ lws_create_context(const struct lws_context_creation_info *info)
 #if defined(LWS_WITH_NETWORK)
 	context->undestroyed_threads = count_threads;
 	context->count_threads = count_threads;
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	context->detailed_latency_cb = info->detailed_latency_cb;
-	context->detailed_latency_filepath = info->detailed_latency_filepath;
-	context->latencies_fd = -1;
-#endif
+
 #if defined(LWS_ROLE_WS) && defined(LWS_WITHOUT_EXTENSIONS)
         if (info->extensions)
                 lwsl_warn("%s: LWS_WITHOUT_EXTENSIONS but extensions ptr set\n", __func__);
@@ -1144,12 +1198,6 @@ lws_create_context(const struct lws_context_creation_info *info)
 #endif
 #endif
 
-#if defined(LWS_WITH_STATS)
-	context->pt[0].sul_stats.cb = lws_sul_stats_cb;
-	__lws_sul_insert_us(&context->pt[0].pt_sul_owner[LWSSULLI_MISS_IF_SUSPENDED],
-			    &context->pt[0].sul_stats, 10 * LWS_US_PER_SEC);
-#endif
-
 #if defined(LWS_HAVE_SYS_CAPABILITY_H) && defined(LWS_HAVE_LIBCAP)
 	memcpy(context->caps, info->caps, sizeof(context->caps));
 	context->count_caps = info->count_caps;
@@ -1364,8 +1412,10 @@ bail_libuv_aware:
 	return NULL;
 #endif
 
+#if defined(LWS_WITH_NETWORK)
 fail_event_libs:
 	lwsl_err("Requested event library support not configured\n");
+#endif
 
 free_context_fail:
 	lws_free(context);
@@ -1598,6 +1648,11 @@ lws_context_destroy(struct lws_context *context)
 		context->being_destroyed = 1;
 
 #if defined(LWS_WITH_NETWORK)
+
+#if defined(LWS_WITH_SYS_METRICS)
+		lws_metrics_dump(context);
+#endif
+
 		/*
 		 * Close any vhost listen wsi
 		 *
@@ -1819,7 +1874,6 @@ next:
 		lwsl_debug("%p: post pdl\n", __func__);
 #endif
 
-		lws_stats_log_dump(context);
 #if defined(LWS_WITH_NETWORK)
 		lws_ssl_context_destroy(context);
 #endif
@@ -1982,6 +2036,10 @@ next:
 		lws_mutex_refcount_destroy(&context->mr);
 #endif
 
+#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_NETWORK)
+		lws_metrics_destroy(context);
+#endif
+
 		if (context->external_baggage_free_on_destroy)
 			free(context->external_baggage_free_on_destroy);
 
diff --git a/lib/core/logs.c b/lib/core/logs.c
index 019858ddfd47d807f42af54fde6172a98fb327e3..e6b6025c5ceb64c5b9bbc032863b3bb186261282 100644
--- a/lib/core/logs.c
+++ b/lib/core/logs.c
@@ -55,6 +55,42 @@ __lws_lc_tag(lws_lifecycle_group_t *grp, lws_lifecycle_t *lc,
 	va_list ap;
 	int n = 1;
 
+	if (*lc->gutag == '[') {
+		/* appending inside [] */
+
+		char *cp = strchr(lc->gutag, ']');
+		char rend[96];
+		size_t ll, k;
+		int n;
+
+		if (!cp)
+			return;
+
+		/* length of closing brace and anything else after it */
+		k = strlen(cp);
+
+		/* compute the remaining gutag unused */
+		ll = sizeof(lc->gutag) - lws_ptr_diff_size_t(cp, lc->gutag) - k - 1;
+		if (ll > sizeof(rend) - 1)
+			ll = sizeof(rend) - 1;
+		va_start(ap, format);
+		n = vsnprintf(rend, ll, format, ap);
+		va_end(ap);
+
+		if ((unsigned int)n > ll)
+			n = (int)ll;
+
+		/* shove the trailer up by what we added */
+		memmove(cp + n, cp, k);
+		assert(k + (unsigned int)n < sizeof(lc->gutag));
+		cp[k + (unsigned int)n] = '\0';
+		/* copy what we added into place */
+		memcpy(cp, rend, (unsigned int)n);
+
+		return;
+	}
+
+	assert(grp);
 	assert(grp->tag_prefix); /* lc group must have a tag prefix string */
 
 	lc->gutag[0] = '[';
diff --git a/lib/core/private-lib-core.h b/lib/core/private-lib-core.h
index 47ad0b5e1b406cdd7e3f06edc55d62218dea40ea..0ec908963db812672e836dd52a233e2ffd9ccb9b 100644
--- a/lib/core/private-lib-core.h
+++ b/lib/core/private-lib-core.h
@@ -139,6 +139,41 @@
 
 #include "libwebsockets.h"
 
+/*
+ * lws_dsh
+*/
+
+typedef struct lws_dsh_obj_head {
+	lws_dll2_owner_t		owner;
+	size_t				total_size; /* for this kind in dsh */
+	int				kind;
+} lws_dsh_obj_head_t;
+
+typedef struct lws_dsh_obj {
+	lws_dll2_t			list;	/* must be first */
+	struct lws_dsh	  		*dsh;	/* invalid when on free list */
+	size_t				size;	/* invalid when on free list */
+	size_t				asize;
+	int				kind; /* so we can account at free */
+} lws_dsh_obj_t;
+
+typedef struct lws_dsh {
+	lws_dll2_t			list;
+	uint8_t				*buf;
+	lws_dsh_obj_head_t		*oha;	/* array of object heads/kind */
+	size_t				buffer_size;
+	size_t				locally_in_use;
+	size_t				locally_free;
+	int				count_kinds;
+	uint8_t				being_destroyed;
+	/*
+	 * Overallocations at create:
+	 *
+	 *  - the buffer itself
+	 *  - the object heads array
+	 */
+} lws_dsh_t;
+
  /*
   *
   *  ------ lifecycle defines ------
@@ -285,6 +320,9 @@ struct lws;
 #include "private-lib-system-fault-injection.h"
 #endif
 
+#include "private-lib-system-metrics.h"
+
+
 struct lws_foreign_thread_pollfd {
 	struct lws_foreign_thread_pollfd *next;
 	int fd_index;
@@ -328,6 +366,10 @@ enum {
 
 	LWSLCG_WSI_SERVER,		/* server wsi */
 
+#if defined(LWS_ROLE_H2) || defined(LWS_ROLE_MQTT)
+	LWSLCG_WSI_MUX,			/* a mux child wsi */
+#endif
+
 #if defined(LWS_WITH_CLIENT)
 	LWSLCG_WSI_CLIENT,		/* client wsi */
 #endif
@@ -418,21 +460,53 @@ struct lws_context {
 	struct http2_settings			set;
 #endif
 
-#if defined(LWS_WITH_SERVER_STATUS)
-	struct lws_conn_stats			conn_stats;
-#endif
 #if LWS_MAX_SMP > 1
 	struct lws_mutex_refcount		mr;
 #endif
 
-#if defined(LWS_WITH_NETWORK)
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_dll2_owner_t			owner_mtr_dynpol;
+	/**< owner for lws_metric_policy_dyn_t (dynamic part of metric pols) */
+	lws_dll2_owner_t			owner_mtr_no_pol;
+	/**< owner for lws_metric_pub_t with no policy to bind to */
+#endif
 
+#if defined(LWS_WITH_NETWORK)
 /*
  * LWS_WITH_NETWORK =====>
  */
 
 	lws_dll2_owner_t		owner_vh_being_destroyed;
 
+	lws_metric_t			*mt_service; /* doing service */
+	const lws_metric_policy_t	*metrics_policies;
+	const char			*metrics_prefix;
+
+#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_CLIENT)
+	lws_metric_t			*mt_conn_tcp; /* client tcp conns */
+	lws_metric_t			*mt_conn_tls; /* client tcp conns */
+	lws_metric_t			*mt_conn_dns; /* client dns external lookups */
+	lws_metric_t			*mth_conn_failures; /* histogram of conn failure reasons */
+#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
+	lws_metric_t			*mt_http_txn; /* client http transaction */
+#endif
+#if defined(LWS_WITH_SYS_ASYNC_DNS)
+	lws_metric_t			*mt_adns_cache; /* async dns lookup lat */
+#endif
+#if defined(LWS_WITH_SECURE_STREAMS)
+	lws_metric_t			*mth_ss_conn; /* SS connection outcomes */
+#endif
+#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API)
+	lws_metric_t			*mt_ss_cliprox_conn; /* SS cli->prox conn */
+	lws_metric_t			*mt_ss_cliprox_paylat; /* cli->prox payload latency */
+	lws_metric_t			*mt_ss_proxcli_paylat; /* prox->cli payload latency */
+#endif
+#endif /* client */
+
+#if defined(LWS_WITH_SERVER)
+	lws_metric_t			*mth_srv;
+#endif
+
 #if defined(LWS_WITH_EVENT_LIBS)
 	struct lws_plugin		*evlib_plugin_list;
 	void				*evlib_ctx; /* overallocated */
@@ -498,9 +572,6 @@ struct lws_context {
 	const struct lws_tls_ops	*tls_ops;
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	det_lat_buf_cb_t		detailed_latency_cb;
-#endif
 #if defined(LWS_WITH_PLUGINS)
 	struct lws_plugin		*plugin_list;
 #endif
@@ -531,9 +602,6 @@ struct lws_context {
 #if !defined(LWS_PLAT_FREERTOS)
 	const char *username, *groupname;
 #endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	const char *detailed_latency_filepath;
-#endif
 
 #if defined(LWS_AMAZON_RTOS) && defined(LWS_WITH_MBEDTLS)
 	mbedtls_entropy_context mec;
@@ -589,6 +657,9 @@ struct lws_context {
 	uint64_t options;
 
 	time_t last_ws_ping_pong_check_s;
+#if defined(LWS_WITH_SECURE_STREAMS)
+	time_t					last_policy;
+#endif
 
 #if defined(LWS_PLAT_FREERTOS)
 	unsigned long time_last_state_dump;
@@ -607,9 +678,6 @@ struct lws_context {
 	int count_cgi_spawned;
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	int latencies_fd;
-#endif
 	unsigned int fd_limit_per_thread;
 	unsigned int timeout_secs;
 	unsigned int pt_serv_buf_size;
@@ -667,10 +735,6 @@ struct lws_context {
 	uint8_t captive_portal_detect_type;
 
 	uint8_t		destroy_state; /* enum lws_context_destroy */
-
-#if defined(LWS_WITH_STATS)
-	uint8_t updated;
-#endif
 };
 
 #define lws_get_context_protocol(ctx, x) ctx->vhost_list->protocols[x]
diff --git a/lib/plat/freertos/freertos-service.c b/lib/plat/freertos/freertos-service.c
index 1d79301b4997310c220e5e540fa26e55bc76823e..1bea562a95cc039f9c70c3023d99cc5d7bc43098 100644
--- a/lib/plat/freertos/freertos-service.c
+++ b/lib/plat/freertos/freertos-service.c
@@ -50,7 +50,6 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi)
 		return 1;
 
 	pt = &context->pt[tsi];
-	lws_stats_bump(pt, LWSSTATS_C_SERVICE_ENTRY, 1);
 
 	{
 		unsigned long m = lws_now_secs();
@@ -142,15 +141,6 @@ again:
 			n = select(max_fd + 1, &readfds, &writefds, &errfds, ptv);
 			n = 0;
 
-	#if defined(LWS_WITH_DETAILED_LATENCY)
-			/*
-			 * so we can track how long it took before we actually read a POLLIN
-			 * that was signalled when we last exited poll()
-			 */
-			if (context->detailed_latency_cb)
-				pt->ust_left_poll = lws_now_usecs();
-	#endif
-
 			for (m = 0; m < (int)pt->fds_count; m++) {
 				c = 0;
 				if (FD_ISSET(pt->fds[m].fd, &readfds)) {
diff --git a/lib/plat/unix/unix-service.c b/lib/plat/unix/unix-service.c
index c11bbad80b898ffdde01a88e9ddb135de418096f..49d06920b5b8aa4d994250d4b29679b1fad11a4e 100644
--- a/lib/plat/unix/unix-service.c
+++ b/lib/plat/unix/unix-service.c
@@ -71,6 +71,9 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi)
 	volatile struct lws_context_per_thread *vpt;
 	struct lws_context_per_thread *pt;
 	lws_usec_t timeout_us, us;
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_usec_t a, b;
+#endif
 	int n;
 #if (defined(LWS_ROLE_WS) && !defined(LWS_WITHOUT_EXTENSIONS)) || defined(LWS_WITH_TLS)
 	int m;
@@ -81,11 +84,14 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi)
 	if (!context)
 		return 1;
 
+#if defined(LWS_WITH_SYS_METRICS)
+	b =
+#endif
+			us = lws_now_usecs();
+
 	pt = &context->pt[tsi];
 	vpt = (volatile struct lws_context_per_thread *)pt;
 
-	lws_stats_bump(pt, LWSSTATS_C_SERVICE_ENTRY, 1);
-
 	if (timeout_ms < 0)
 		timeout_ms = 0;
 	else
@@ -108,7 +114,6 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi)
 		pt->service_tid_detected = 1;
 	}
 
-	us = lws_now_usecs();
 	lws_pt_lock(pt, __func__);
 	/*
 	 * service ripe scheduled events, and limit wait to next expected one
@@ -144,15 +149,9 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi)
 	vpt->inside_poll = 0;
 	lws_memory_barrier();
 
-	#if defined(LWS_WITH_DETAILED_LATENCY)
-	/*
-	 * so we can track how long it took before we actually read a
-	 * POLLIN that was signalled when we last exited poll()
-	 */
-	if (context->detailed_latency_cb)
-		pt->ust_left_poll = lws_now_usecs();
+#if defined(LWS_WITH_SYS_METRICS)
+	b = lws_now_usecs();
 #endif
-
 	/* Collision will be rare and brief.  Spin until it completes */
 	while (vpt->foreign_spinlock)
 		;
@@ -207,14 +206,16 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi)
 #if (defined(LWS_ROLE_WS) && !defined(LWS_WITHOUT_EXTENSIONS)) || defined(LWS_WITH_TLS)
 		!m &&
 #endif
-		!n) { /* nothing to do */
+		!n) /* nothing to do */
 		lws_service_do_ripe_rxflow(pt);
+	else
+		if (_lws_plat_service_forced_tsi(context, tsi) < 0)
+			return -1;
 
-		return 0;
-	}
-
-	if (_lws_plat_service_forced_tsi(context, tsi) < 0)
-		return -1;
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metric_event(context->mt_service, METRES_GO,
+			 (u_mt_t) (a + (lws_now_usecs() - b)));
+#endif
 
 	if (pt->destroy_self) {
 		lws_context_destroy(pt->context);
diff --git a/lib/roles/h1/ops-h1.c b/lib/roles/h1/ops-h1.c
index ea69163f4933200de294241f0f7be6ed08644539..a6e53e40cfa8ee3abac4a9dffbf6be5cfe646e4b 100644
--- a/lib/roles/h1/ops-h1.c
+++ b/lib/roles/h1/ops-h1.c
@@ -521,19 +521,6 @@ try_pollout:
 			return LWS_HPI_RET_HANDLED;
 		}
 
-		lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB, 1);
-#if defined(LWS_WITH_STATS)
-		if (wsi->active_writable_req_us) {
-			uint64_t ul = lws_now_usecs() -
-					wsi->active_writable_req_us;
-
-			lws_stats_bump(pt, LWSSTATS_US_WRITABLE_DELAY_AVG, ul);
-			lws_stats_max(pt,
-				  LWSSTATS_US_WORST_WRITABLE_DELAY, ul);
-			wsi->active_writable_req_us = 0;
-		}
-#endif
-
 		n = user_callback_handle_rxflow(wsi->a.protocol->callback, wsi,
 						LWS_CALLBACK_HTTP_WRITEABLE,
 						wsi->user_space, NULL, 0);
@@ -934,6 +921,7 @@ rops_adoption_bind_h1(struct lws *wsi, int type, const char *vh_prot_name)
 #if defined(LWS_WITH_HTTP2)
 	if ((!(type & LWS_ADOPT_ALLOW_SSL)) && (wsi->a.vhost->options & LWS_SERVER_OPTION_H2_PRIOR_KNOWLEDGE)) {
 		lwsl_info("http/2 prior knowledge\n");
+		lws_metrics_tag_wsi_add(wsi, "upg", "h2_prior");
 		lws_role_call_alpn_negotiated(wsi, "h2");
 	}
 	else
diff --git a/lib/roles/h2/hpack.c b/lib/roles/h2/hpack.c
index 326f9dd0523bc1a6ca95a051e71c49211626ed88..68629e6f47595a5689602651fc8de10fb993a89d 100644
--- a/lib/roles/h2/hpack.c
+++ b/lib/roles/h2/hpack.c
@@ -1422,7 +1422,7 @@ int lws_add_http2_header_by_name(struct lws *wsi, const unsigned char *name,
 
 	*((*p)++) = 0; /* literal hdr, literal name,  */
 
-	*((*p)++) = 0 | (uint8_t)lws_h2_num_start(7, (unsigned long)len); /* non-HUF */
+	*((*p)++) = (uint8_t)(0 | (uint8_t)lws_h2_num_start(7, (unsigned long)len)); /* non-HUF */
 	if (lws_h2_num(7, (unsigned long)len, p, end))
 		return 1;
 
@@ -1432,7 +1432,7 @@ int lws_add_http2_header_by_name(struct lws *wsi, const unsigned char *name,
 	while(len--)
 		*((*p)++) = (uint8_t)tolower((int)*name++);
 
-	*((*p)++) = 0 | (uint8_t)lws_h2_num_start(7, (unsigned long)length); /* non-HUF */
+	*((*p)++) = (uint8_t)(0 | (uint8_t)lws_h2_num_start(7, (unsigned long)length)); /* non-HUF */
 	if (lws_h2_num(7, (unsigned long)length, p, end))
 		return 1;
 
diff --git a/lib/roles/h2/http2.c b/lib/roles/h2/http2.c
index 07d0ef6fea79c7737e507e40ccca49f94e7ee017..ba836e958a05aff335583a6c5dad0bb7a2a73e9c 100644
--- a/lib/roles/h2/http2.c
+++ b/lib/roles/h2/http2.c
@@ -261,6 +261,11 @@ lws_wsi_server_new(struct lws_vhost *vh, struct lws *parent_wsi,
 		return NULL;
 	}
 
+#if defined(LWS_WITH_SERVER)
+	if (lwsi_role_server(parent_wsi))
+		lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mth_srv);
+#endif
+
 	h2n->highest_sid_opened = sid;
 
 	lws_wsi_mux_insert(wsi, parent_wsi, sid);
@@ -281,10 +286,6 @@ lws_wsi_server_new(struct lws_vhost *vh, struct lws *parent_wsi,
 	if (lws_ensure_user_space(wsi))
 		goto bail1;
 
-#if defined(LWS_WITH_SERVER_STATUS)
-	wsi->a.vhost->conn_stats.h2_subs++;
-#endif
-
 #if defined(LWS_WITH_SERVER) && defined(LWS_WITH_SECURE_STREAMS)
 	if (lws_adopt_ss_server_accept(wsi))
 		goto bail1;
@@ -362,10 +363,6 @@ lws_wsi_h2_adopt(struct lws *parent_wsi, struct lws *wsi)
 
 	lws_callback_on_writable(wsi);
 
-#if defined(LWS_WITH_SERVER_STATUS)
-	wsi->a.vhost->conn_stats.h2_subs++;
-#endif
-
 	return wsi;
 
 bail1:
@@ -818,9 +815,6 @@ int lws_h2_do_pps_send(struct lws *wsi)
 			h2n->swsi->h2.END_STREAM = 1;
 			lwsl_info("servicing initial http request\n");
 
-#if defined(LWS_WITH_SERVER_STATUS)
-			wsi->a.vhost->conn_stats.h2_trans++;
-#endif
 #if defined(LWS_WITH_SERVER)
 			if (lws_http_action(h2n->swsi))
 				goto bail;
@@ -1713,9 +1707,6 @@ lws_h2_parse_end_of_frame(struct lws *wsi)
 		lws_http_compression_validate(h2n->swsi);
 #endif
 
-#if defined(LWS_WITH_SERVER_STATUS)
-		wsi->a.vhost->conn_stats.h2_trans++;
-#endif
 		p = lws_hdr_simple_ptr(h2n->swsi, WSI_TOKEN_HTTP_COLON_METHOD);
 		/*
 		 * duplicate :path into the individual method uri header
@@ -2532,10 +2523,6 @@ lws_h2_client_handshake(struct lws *wsi)
 	if (lws_finalize_http_header(wsi, &p, end))
 		goto fail_length;
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
-
 	m = LWS_WRITE_HTTP_HEADERS;
 #if defined(LWS_WITH_CLIENT)
 	/* below is not needed in spec, indeed it destroys the long poll
diff --git a/lib/roles/h2/ops-h2.c b/lib/roles/h2/ops-h2.c
index d5a2eba3a2d7e30ef068e7ed4566074e036207d9..f46cfcca04da171844c84df398f485a73b90d008 100644
--- a/lib/roles/h2/ops-h2.c
+++ b/lib/roles/h2/ops-h2.c
@@ -521,13 +521,12 @@ rops_check_upgrades_h2(struct lws *wsi)
 	if (!p || strcmp(p, "websocket"))
 		return LWS_UPG_RET_CONTINUE;
 
-#if defined(LWS_WITH_SERVER_STATUS)
-	wsi->a.vhost->conn_stats.ws_upg++;
-#endif
 	lwsl_info("Upgrade h2 to ws\n");
 	lws_mux_mark_immortal(wsi);
 	wsi->h2_stream_carries_ws = 1;
 
+	lws_metrics_tag_wsi_add(wsi, "upg", "ws_over_h2");
+
 	if (lws_process_ws_upgrade(wsi))
 		return LWS_UPG_RET_BAIL;
 
@@ -1254,9 +1253,6 @@ rops_alpn_negotiated_h2(struct lws *wsi, const char *alpn)
 #endif
 
 	wsi->upgraded_to_http2 = 1;
-#if defined(LWS_WITH_SERVER_STATUS)
-	wsi->a.vhost->conn_stats.h2_alpn++;
-#endif
 
 	/* adopt the header info */
 
diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c
index dfb2105f1f1003ac1e4d1da64712103d7a0f55ca..1e6da7962a949b35b833ec286e33402ebceffb48 100644
--- a/lib/roles/http/client/client-http.c
+++ b/lib/roles/http/client/client-http.c
@@ -67,7 +67,10 @@ lws_http_client_socket_service(struct lws *wsi, struct lws_pollfd *pollfd)
 		 * timeout protection set in client-handshake.c
 		 */
 		if (pollfd->revents & LWS_POLLOUT)
-			lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL);
+			if (lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL) == NULL) {
+				lwsl_client("closed\n");
+				return -1;
+			}
 		break;
 
 #if defined(LWS_WITH_SOCKS5)
@@ -203,16 +206,6 @@ start_ws_handshake:
 			}
 		}
 #endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-		if (context->detailed_latency_cb) {
-			wsi->detlat.type = LDLT_TLS_NEG_CLIENT;
-			wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] =
-				(uint32_t)(lws_now_usecs() -
-				wsi->detlat.earliest_write_req_pre_write);
-			wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-			lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-		}
-#endif
 
 #if defined (LWS_WITH_HTTP2)
 		if (wsi->client_h2_alpn && lwsi_state(wsi) != LRS_H1C_ISSUE_HANDSHAKE2) {
@@ -271,9 +264,7 @@ hs2:
 			  "(wsistate 0x%lx), w sock %d\n",
 			  __func__, lws_wsi_tag(wsi),
 			  (unsigned long)wsi->wsistate, wsi->desc.sockfd);
-#if defined(LWS_WITH_DETAILED_LATENCY)
-		wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
+
 		n = lws_ssl_capable_write(wsi, (unsigned char *)sb, lws_ptr_diff_size_t(p, sb));
 		switch (n) {
 		case LWS_SSL_CAPABLE_ERROR:
@@ -485,6 +476,10 @@ lws_http_transaction_completed_client(struct lws *wsi)
 	lwsl_info("%s: %s (%s)\n", __func__, lws_wsi_tag(wsi),
 			wsi->a.protocol->name);
 
+	// if (wsi->http.ah && wsi->http.ah->http_response)
+	/* we're only judging if any (200, or 500 etc) http txn completed */
+	lws_metrics_caliper_report(wsi->cal_conn, METRES_GO);
+
 	if (user_callback_handle_rxflow(wsi->a.protocol->callback, wsi,
 					LWS_CALLBACK_COMPLETED_CLIENT_HTTP,
 					wsi->user_space, NULL, 0)) {
@@ -1210,6 +1205,8 @@ lws_generate_client_handshake(struct lws *wsi, char *pkt)
 	if (wsi->client_http_body_pending)
 		lws_callback_on_writable(wsi);
 
+	lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mt_http_txn);
+
 	// puts(pkt);
 
 	return p;
diff --git a/lib/roles/http/header.c b/lib/roles/http/header.c
index eb84d7cf11db2ee1566daab532c66c359e7736cd..83983a573d4165181643959e90111e9ad577b6a2 100644
--- a/lib/roles/http/header.c
+++ b/lib/roles/http/header.c
@@ -125,9 +125,6 @@ lws_finalize_write_http_header(struct lws *wsi, unsigned char *start,
 	p = *pp;
 	len = lws_ptr_diff(p, start);
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
 	if (lws_write(wsi, start, (unsigned int)len, LWS_WRITE_HTTP_HEADERS) != len)
 		return 1;
 
@@ -316,6 +313,7 @@ lws_add_http_header_status(struct lws *wsi, unsigned int _code,
 	unsigned char code_and_desc[60];
 	int n;
 
+	wsi->http.response_code = code;
 #ifdef LWS_WITH_ACCESS_LOG
 	wsi->http.access_log.response = (int)code;
 #endif
@@ -482,9 +480,6 @@ lws_return_http_status(struct lws *wsi, unsigned int code,
 		 *
 		 * Solve it by writing the headers now...
 		 */
-#if defined(LWS_WITH_DETAILED_LATENCY)
-		wsi->detlat.earliest_write_req_pre_write = lws_now_usecs();
-#endif
 		m = lws_write(wsi, start, lws_ptr_diff_size_t(p, start),
 			      LWS_WRITE_HTTP_HEADERS);
 		if (m != lws_ptr_diff(p, start))
diff --git a/lib/roles/http/parsers.c b/lib/roles/http/parsers.c
index 778d96a5825a2ef5ad48155fe1f506ee842e40cb..2999faa2bed5407086f7ebf1e780ad35de3cae93 100644
--- a/lib/roles/http/parsers.c
+++ b/lib/roles/http/parsers.c
@@ -236,11 +236,8 @@ lws_header_table_attach(struct lws *wsi, int autoservice)
 
 	n = pt->http.ah_count_in_use == (int)context->max_http_header_pool;
 #if defined(LWS_WITH_PEER_LIMITS)
-	if (!n) {
+	if (!n)
 		n = lws_peer_confirm_ah_attach_ok(context, wsi->peer);
-		if (n)
-			lws_stats_bump(pt, LWSSTATS_C_PEER_LIMIT_AH_DENIED, 1);
-	}
 #endif
 	if (n) {
 		/*
@@ -375,12 +372,7 @@ int __lws_header_table_detach(struct lws *wsi, int autoservice)
 			wsi = *pwsi;
 			pwsi_eligible = pwsi;
 		}
-#if defined(LWS_WITH_PEER_LIMITS)
-		else
-			if (!(*pwsi)->http.ah_wait_list)
-				lws_stats_bump(pt,
-					LWSSTATS_C_PEER_LIMIT_AH_DENIED, 1);
-#endif
+
 		pwsi = &(*pwsi)->http.ah_wait_list;
 	}
 
diff --git a/lib/roles/http/private-lib-roles-http.h b/lib/roles/http/private-lib-roles-http.h
index d8d9fc17e5ad10f841fb952a1ce5216f37f5b565..94ee876896046c726b2b1f5eddbfd3b922199e7c 100644
--- a/lib/roles/http/private-lib-roles-http.h
+++ b/lib/roles/http/private-lib-roles-http.h
@@ -248,6 +248,9 @@ struct _lws_http_mode_related {
 #ifdef LWS_WITH_ACCESS_LOG
 	struct lws_access_log access_log;
 #endif
+#if defined(LWS_WITH_SERVER)
+	unsigned int response_code;
+#endif
 #ifdef LWS_WITH_CGI
 	struct lws_cgi *cgi; /* wsi being cgi stream have one of these */
 #endif
diff --git a/lib/roles/http/server/server.c b/lib/roles/http/server/server.c
index c93d43c49eb50c615a8f2e8192e6f1c22e9ba11d..c6b4bf63289dfc0c6ce8a63623bd159139d8e17b 100644
--- a/lib/roles/http/server/server.c
+++ b/lib/roles/http/server/server.c
@@ -797,6 +797,10 @@ lws_find_mount(struct lws *wsi, const char *uri_ptr, int uri_len)
 		     uri_ptr[hm->mountpoint_len] == '/' ||
 		     hm->mountpoint_len == 1)
 		    ) {
+#if defined(LWS_WITH_SYS_METRICS)
+			lws_metrics_tag_wsi_add(wsi, "mnt", hm->mountpoint);
+#endif
+
 			if (hm->origin_protocol == LWSMPRO_CALLBACK ||
 			    ((hm->origin_protocol == LWSMPRO_CGI ||
 			     lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI) ||
@@ -1416,6 +1420,9 @@ lws_http_action(struct lws *wsi)
 	if (meth < 0 || meth >= (int)LWS_ARRAY_SIZE(method_names))
 		goto bail_nuke_ah;
 
+	lws_metrics_tag_wsi_add(wsi, "vh", wsi->a.vhost->name);
+	lws_metrics_tag_wsi_add(wsi, "meth", method_names[meth]);
+
 	/* we insist on absolute paths */
 
 	if (!uri_ptr || uri_ptr[0] != '/') {
@@ -2076,17 +2083,9 @@ raw_transition:
 		} else
 			lwsl_info("no host\n");
 
-		if (!lwsi_role_h2(wsi) || !lwsi_role_server(wsi)) {
-#if defined(LWS_WITH_SERVER_STATUS)
-			wsi->a.vhost->conn_stats.h1_trans++;
-#endif
-			if (!wsi->conn_stat_done) {
-#if defined(LWS_WITH_SERVER_STATUS)
-				wsi->a.vhost->conn_stats.h1_conn++;
-#endif
-				wsi->conn_stat_done = 1;
-			}
-		}
+		if ((!lwsi_role_h2(wsi) || !lwsi_role_server(wsi)) &&
+		    (!wsi->conn_stat_done))
+			wsi->conn_stat_done = 1;
 
 		/* check for unwelcome guests */
 #if defined(LWS_WITH_HTTP_UNCOMMON_HEADERS)
@@ -2121,9 +2120,6 @@ raw_transition:
 							uri_ptr, uri_len, meth);
 
 					/* wsi close will do the log */
-#endif
-#if defined(LWS_WITH_SERVER_STATUS)
-					wsi->a.vhost->conn_stats.rejected++;
 #endif
 					/*
 					 * We don't want anything from
@@ -2215,18 +2211,14 @@ raw_transition:
 
 			if (!strcasecmp(up, "websocket")) {
 #if defined(LWS_ROLE_WS)
-#if defined(LWS_WITH_SERVER_STATUS)
-				wsi->a.vhost->conn_stats.ws_upg++;
-#endif
+				lws_metrics_tag_wsi_add(wsi, "upg", "ws");
 				lwsl_info("Upgrade to ws\n");
 				goto upgrade_ws;
 #endif
 			}
 #if defined(LWS_WITH_HTTP2)
 			if (!strcasecmp(up, "h2c")) {
-#if defined(LWS_WITH_SERVER_STATUS)
-				wsi->a.vhost->conn_stats.h2_upg++;
-#endif
+				lws_metrics_tag_wsi_add(wsi, "upg", "h2c");
 				lwsl_info("Upgrade to h2c\n");
 				goto upgrade_h2c;
 			}
@@ -2379,6 +2371,15 @@ lws_http_transaction_completed(struct lws *wsi)
 		return 0;
 	}
 
+#if defined(LWS_WITH_SYS_METRICS)
+	{
+		char tmp[10];
+
+		lws_snprintf(tmp, sizeof(tmp), "%u", wsi->http.response_code);
+		lws_metrics_tag_wsi_add(wsi, "status", tmp);
+	}
+#endif
+
 	lwsl_info("%s: %s\n", __func__, lws_wsi_tag(wsi));
 
 #if defined(LWS_WITH_HTTP_STREAM_COMPRESSION)
diff --git a/lib/roles/mqtt/client/client-mqtt.c b/lib/roles/mqtt/client/client-mqtt.c
index 8e87218f8775acc6dc13b6b76586752220f01d1b..93b539eb33f2a7c39271533289a09eb0472dcb51 100644
--- a/lib/roles/mqtt/client/client-mqtt.c
+++ b/lib/roles/mqtt/client/client-mqtt.c
@@ -264,17 +264,6 @@ lws_mqtt_client_socket_service(struct lws *wsi, struct lws_pollfd *pollfd,
 			wsi->tls.ssl = NULL;
 #endif /* LWS_WITH_TLS */
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-		if (context->detailed_latency_cb) {
-			wsi->detlat.type = LDLT_TLS_NEG_CLIENT;
-			wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] =
-				(uint32_t)(lws_now_usecs() -
-				wsi->detlat.earliest_write_req_pre_write);
-			wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-			lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-		}
-#endif
-
 		/* fallthru */
 
 #if defined(LWS_WITH_SOCKS5)
diff --git a/lib/roles/mqtt/mqtt.c b/lib/roles/mqtt/mqtt.c
index b47bbe93e5da9f4baeb4390090c8f3c4453d6009..02a648263de7c5ca525e5d74c514678aaeee9234 100644
--- a/lib/roles/mqtt/mqtt.c
+++ b/lib/roles/mqtt/mqtt.c
@@ -992,7 +992,7 @@ cmd_completion:
 				lws_set_timeout(wsi, 0, 0);
 
 				w = lws_create_new_server_wsi(wsi->a.vhost,
-							      wsi->tsi, "mqtt");
+							      wsi->tsi, "mqtt_sid1");
 				if (!w) {
 					lwsl_notice("%s: sid 1 migrate failed\n",
 							__func__);
@@ -1042,10 +1042,6 @@ cmd_completion:
 						__func__, lws_wsi_tag(wsi),
 						lws_wsi_tag(w));
 
-			#if defined(LWS_WITH_SERVER_STATUS)
-				wsi->a.vhost->conn_stats.h2_subs++;
-			#endif
-
 				/*
 				 * It was the last thing we were waiting for
 				 * before we can be fully ESTABLISHED
@@ -2107,10 +2103,6 @@ lws_wsi_mqtt_adopt(struct lws *parent_wsi, struct lws *wsi)
 	lws_mqtt_set_client_established(wsi);
 	lws_callback_on_writable(wsi);
 
-#if defined(LWS_WITH_SERVER_STATUS)
-	wsi->a.vhost->conn_stats.mqtt_subs++;
-#endif
-
 	return wsi;
 
 bail1:
diff --git a/lib/roles/raw-skt/ops-raw-skt.c b/lib/roles/raw-skt/ops-raw-skt.c
index 229f5af7b7a5fab2bdedefb9e92365dc1da7fca0..cbe927cf4ea191f9e3cb8548d5d9d6a61eb6c162 100644
--- a/lib/roles/raw-skt/ops-raw-skt.c
+++ b/lib/roles/raw-skt/ops-raw-skt.c
@@ -120,6 +120,8 @@ rops_handle_POLLIN_raw_skt(struct lws_context_per_thread *pt, struct lws *wsi,
 			buffered = lws_buflist_aware_read(pt, wsi, &ebuf, 1, __func__);
 			switch (ebuf.len) {
 			case 0:
+				if (wsi->unix_skt)
+					break;
 				lwsl_info("%s: read 0 len\n", __func__);
 				wsi->seen_zero_length_recv = 1;
 				if (lws_change_pollfd(wsi, LWS_POLLIN, 0))
@@ -202,18 +204,6 @@ try_pollout:
 	/* clear back-to-back write detection */
 	wsi->could_have_pending = 0;
 
-	lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB, 1);
-#if defined(LWS_WITH_STATS)
-	if (wsi->active_writable_req_us) {
-		uint64_t ul = lws_now_usecs() -
-				wsi->active_writable_req_us;
-
-		lws_stats_bump(pt, LWSSTATS_US_WRITABLE_DELAY_AVG, ul);
-		lws_stats_max(pt,
-			  LWSSTATS_US_WORST_WRITABLE_DELAY, ul);
-		wsi->active_writable_req_us = 0;
-	}
-#endif
 	n = user_callback_handle_rxflow(wsi->a.protocol->callback,
 			wsi, LWS_CALLBACK_RAW_WRITEABLE,
 			wsi->user_space, NULL, 0);
diff --git a/lib/roles/ws/client-ws.c b/lib/roles/ws/client-ws.c
index af733cc7c7412badef4db57e739f67999fa67c17..7a795b615096ec41eda97bc833dd05c0b82af234 100644
--- a/lib/roles/ws/client-ws.c
+++ b/lib/roles/ws/client-ws.c
@@ -251,11 +251,6 @@ lws_client_ws_upgrade(struct lws *wsi, const char **cce)
 	char ignore;
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-		wsi->detlat.earliest_write_req = 0;
-		wsi->detlat.earliest_write_req_pre_write = 0;
-#endif
-
 	if (wsi->client_mux_substream) {/* !!! client ws-over-h2 not there yet */
 		lwsl_warn("%s: client ws-over-h2 upgrade not supported yet\n",
 			  __func__);
diff --git a/lib/secure-streams/policy-common.c b/lib/secure-streams/policy-common.c
index 14de047cec9f55a169f24927a45f6e05b6f02c7e..18c05ddab78d6f6aa2fe2efd800933ac942a97f4 100644
--- a/lib/secure-streams/policy-common.c
+++ b/lib/secure-streams/policy-common.c
@@ -297,6 +297,17 @@ lws_ss_policy_set(struct lws_context *context, const char *name)
 	if (context->ac_policy) {
 		int n;
 
+#if defined(LWS_WITH_SYS_METRICS)
+		lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
+					   context->owner_mtr_dynpol.head) {
+			lws_metric_policy_dyn_t *dm =
+				lws_container_of(d, lws_metric_policy_dyn_t, list);
+
+			lws_metric_policy_dyn_destroy(dm, 1); /* keep */
+
+		} lws_end_foreach_dll_safe(d, d1);
+#endif
+
 		/*
 		 * any existing ss created with the old policy have to go away
 		 * now, since they point to the shortly-to-be-destroyed old
@@ -431,10 +442,27 @@ lws_ss_policy_set(struct lws_context *context, const char *name)
 		x = x->next;
 	}
 
+	context->last_policy = time(NULL);
+#if defined(LWS_WITH_SYS_METRICS)
+	if (context->pss_policies)
+		((lws_ss_policy_t *)context->pss_policies)->metrics =
+						args->heads[LTY_METRICS].m;
+#endif
+
 	/* and we can discard the parsing args object now, invalidating args */
 
 	lws_free_set_NULL(context->pol_args);
 #endif
 
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metric_rebind_policies(context);
+#endif
+
+#if defined(LWS_WITH_SYS_SMD)
+	(void)lws_smd_msg_printf(context, LWSSMDCL_SYSTEM_STATE,
+				 "{\"policy\":\"updated\",\"ts\":%lu}",
+				   (long)context->last_policy);
+#endif
+
 	return ret;
 }
diff --git a/lib/secure-streams/policy-json.c b/lib/secure-streams/policy-json.c
index d4e3bb31e7a72ab7bc1cf868246d37239bc48a63..3876ae2c9536b560344eef756d5e57daa4c51782 100644
--- a/lib/secure-streams/policy-json.c
+++ b/lib/secure-streams/policy-json.c
@@ -41,6 +41,11 @@ static const char * const lejp_tokens_policy[] = {
 	"certs[].*",
 	"trust_stores[].name",
 	"trust_stores[].stack",
+	"metrics[].name",
+	"metrics[].us_schedule",
+	"metrics[].us_halflife",
+	"metrics[].min_outlier",
+	"metrics[].report",
 	"s[].*.endpoint",
 	"s[].*.via-socks5",
 	"s[].*.protocol",
@@ -135,6 +140,11 @@ typedef enum {
 	LSSPPT_CERTS,
 	LSSPPT_TRUST_STORES_NAME,
 	LSSPPT_TRUST_STORES_STACK,
+	LSSPPT_METRICS_NAME,
+	LSSPPT_METRICS_US_SCHEDULE,
+	LSSPPT_METRICS_US_HALFLIFE,
+	LSSPPT_METRICS_MIN_OUTLIER,
+	LSSPPT_METRICS_REPORT,
 	LSSPPT_ENDPOINT,
 	LSSPPT_VH_VIA_SOCKS5,
 	LSSPPT_PROTOCOL,
@@ -225,6 +235,7 @@ static uint16_t sizes[] = {
 	sizeof(lws_ss_trust_store_t),
 	sizeof(lws_ss_policy_t),
 	sizeof(lws_ss_auth_t),
+	sizeof(lws_metric_policy_t),
 };
 
 static const char * const protonames[] = {
@@ -253,6 +264,25 @@ lws_ss_policy_find_auth_by_name(struct policy_cb_args *a,
 	return NULL;
 }
 
+static int
+lws_ss_policy_alloc_helper(struct policy_cb_args *a, int type)
+{
+	/*
+	 * We do the pointers always as .b union member, all of the
+	 * participating structs begin with .next and .name the same
+	 */
+
+	a->curr[type].b = lwsac_use_zero(&a->ac,
+				sizes[type], POL_AC_GRAIN);
+	if (!a->curr[type].b)
+		return 1;
+
+	a->curr[type].b->next = a->heads[type].b;
+	a->heads[type].b = a->curr[type].b;
+
+	return 0;
+}
+
 static signed char
 lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
 {
@@ -291,6 +321,13 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
 	case LSSPPT_AUTH:
 		n = LTY_AUTH;
 		break;
+	case LSSPPT_METRICS_NAME:
+	case LSSPPT_METRICS_US_SCHEDULE:
+	case LSSPPT_METRICS_US_HALFLIFE:
+	case LSSPPT_METRICS_MIN_OUTLIER:
+	case LSSPPT_METRICS_REPORT:
+		n = LTY_METRICS;
+		break;
 	}
 
 	if (reason == LEJPCB_ARRAY_START &&
@@ -300,13 +337,8 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
 		a->count = 0;
 
 	if (reason == LEJPCB_OBJECT_START && n == LTY_AUTH) {
-		a->curr[n].b = lwsac_use_zero(&a->ac, sizes[n], POL_AC_GRAIN);
-		if (!a->curr[n].b)
+		if (lws_ss_policy_alloc_helper(a, LTY_AUTH))
 			goto oom;
-
-		a->curr[n].b->next = a->heads[n].b;
-		a->heads[n].b = a->curr[n].b;
-
 		return 0;
 	}
 
@@ -360,7 +392,7 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
 	}
 
 	if (reason == LEJPCB_PAIR_NAME && n != -1 &&
-	    (n != LTY_TRUSTSTORE && n != LTY_AUTH)) {
+	    (n != LTY_TRUSTSTORE && n != LTY_AUTH && n != LTY_METRICS)) {
 
 		p2 = NULL;
 		if (n == LTY_POLICY) {
@@ -490,18 +522,10 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
 		break;
 
 	case LSSPPT_TRUST_STORES_NAME:
-		/*
-		 * We do the pointers always as .b, all of the participating
-		 * structs begin with .next and .name
-		 */
-		a->curr[LTY_TRUSTSTORE].b = lwsac_use_zero(&a->ac,
-					sizes[LTY_TRUSTSTORE], POL_AC_GRAIN);
-		if (!a->curr[LTY_TRUSTSTORE].b)
+		if (lws_ss_policy_alloc_helper(a, LTY_TRUSTSTORE))
 			goto oom;
 
 		a->count = 0;
-		a->curr[LTY_TRUSTSTORE].b->next = a->heads[LTY_TRUSTSTORE].b;
-		a->heads[LTY_TRUSTSTORE].b = a->curr[LTY_TRUSTSTORE].b;
 		pp = (char **)&a->curr[LTY_TRUSTSTORE].b->name;
 
 		goto string2;
@@ -528,6 +552,31 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason)
 		lwsl_err("%s: unknown trust store entry %s\n", __func__,
 			 dotstar);
 		goto oom;
+#if defined(LWS_WITH_SYS_METRICS)
+	case LSSPPT_METRICS_NAME:
+		if (lws_ss_policy_alloc_helper(a, LTY_METRICS))
+			goto oom;
+
+		pp = (char **)&a->curr[LTY_METRICS].b->name;
+
+		goto string2;
+
+	case LSSPPT_METRICS_US_SCHEDULE:
+		a->curr[LTY_METRICS].m->us_schedule = (uint32_t)atol(ctx->buf);
+		break;
+
+	case LSSPPT_METRICS_US_HALFLIFE:
+		a->curr[LTY_METRICS].m->us_decay_unit = (uint32_t)atol(ctx->buf);
+		break;
+
+	case LSSPPT_METRICS_MIN_OUTLIER:
+		a->curr[LTY_METRICS].m->min_contributors = (uint8_t)atoi(ctx->buf);
+		break;
+
+	case LSSPPT_METRICS_REPORT:
+		pp = (char **)&a->curr[LTY_METRICS].m->report;
+		goto string2;
+#endif
 
 	case LSSPPT_SERVER_CERT:
 	case LSSPPT_SERVER_KEY:
diff --git a/lib/secure-streams/private-lib-secure-streams.h b/lib/secure-streams/private-lib-secure-streams.h
index ab0b661e4725193f479e017420a919b5a9c97c32..b386776deecf7b2f3c985874efd88ceae88d5e27 100644
--- a/lib/secure-streams/private-lib-secure-streams.h
+++ b/lib/secure-streams/private-lib-secure-streams.h
@@ -49,6 +49,10 @@ typedef struct lws_ss_handle {
 
 	lws_lifecycle_t		lc;
 
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metrics_caliper_compose(cal_txn)
+#endif
+
 	struct lws_dll2		list;	  /**< pt lists active ss */
 	struct lws_dll2		to_list;  /**< pt lists ss with pending to-s */
 #if defined(LWS_WITH_SERVER)
@@ -290,6 +294,10 @@ typedef struct lws_sspc_handle {
 	struct lws_dll2		client_list;
 	struct lws_tx_credit	txc;
 
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metrics_caliper_compose(cal_txn)
+#endif
+
 	struct lws		*cwsi;
 
 	struct lws_dsh		*dsh;
@@ -333,6 +341,7 @@ union u {
 	lws_ss_trust_store_t	*t;
 	lws_ss_policy_t		*p;
 	lws_ss_auth_t		*a;
+	lws_metric_policy_t	*m;
 };
 
 enum {
@@ -341,6 +350,7 @@ enum {
 	LTY_TRUSTSTORE,
 	LTY_POLICY,
 	LTY_AUTH,
+	LTY_METRICS,
 
 	_LTY_COUNT /* always last */
 };
@@ -510,6 +520,29 @@ struct ss_pcols {
 	secstream_protocol_get_txcr_t			tx_cr_est;
 };
 
+/*
+ * Because both sides of the connection share the conn, we allocate it
+ * during accepted adoption, and both sides point to it.
+ *
+ * When .ss or .wsi close, they must NULL their entry here so no dangling
+ * refereneces.
+ *
+ * The last one of the accepted side and the onward side to close frees it.
+ */
+
+
+
+
+struct conn {
+	struct lws_ss_serialization_parser parser;
+
+	lws_dsh_t		*dsh;	/* unified buffer for both sides */
+	struct lws		*wsi;	/* the proxy's client side */
+	lws_ss_handle_t		*ss;	/* the onward, ss side */
+
+	lws_ss_conn_states_t	state;
+};
+
 extern const struct ss_pcols ss_pcol_h1;
 extern const struct ss_pcols ss_pcol_h2;
 extern const struct ss_pcols ss_pcol_ws;
diff --git a/lib/secure-streams/protocols/ss-h1.c b/lib/secure-streams/protocols/ss-h1.c
index 77c8971e361128d2ad7a6e4f171c29e963708f3d..0311dd88536a1b403c89254a2a75f0e2de313ccc 100644
--- a/lib/secure-streams/protocols/ss-h1.c
+++ b/lib/secure-streams/protocols/ss-h1.c
@@ -415,6 +415,7 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 			break;
 		}
 		assert(h->policy);
+		lws_metrics_caliper_report_hist(h->cal_txn, wsi);
 		lwsl_info("%s: %s CLIENT_CONNECTION_ERROR: %s\n", __func__,
 			  h->lc.gutag, in ? (const char *)in : "none");
 		/* already disconnected, no action for DISCONNECT_ME */
@@ -445,8 +446,11 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 			break;
 
 		lws_sul_cancel(&h->sul_timeout);
-		lwsl_notice("%s: %s LWS_CALLBACK_CLOSED_CLIENT_HTTP\n",
-				__func__, wsi->lc.gutag);
+
+		lws_metrics_caliper_report_hist(h->cal_txn, wsi);
+		//lwsl_notice("%s: %s LWS_CALLBACK_CLOSED_CLIENT_HTTP\n",
+		//		__func__, wsi->lc.gutag);
+
 		h->wsi = NULL;
 
 #if defined(LWS_WITH_SERVER)
@@ -487,6 +491,13 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 			/* it's just telling use we connected / joined the nwsi */
 	//		break;
 
+#if defined(LWS_WITH_SYS_METRICS)
+		if (status) {
+			lws_snprintf((char *)buf, 10, "%d", status);
+			lws_metrics_tag_ss_add(h, "http_resp", (char *)buf);
+		}
+#endif
+
 		if (status == HTTP_STATUS_SERVICE_UNAVAILABLE /* 503 */ ||
 		    status == 429 /* Too many requests */) {
 			/*
@@ -552,6 +563,7 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 		lws_sul_cancel(&h->sul);
 
 		if (h->prev_ss_state != LWSSSCS_CONNECTED) {
+			lws_metrics_caliper_report_hist(h->cal_txn, wsi);
 			r = lws_ss_event_helper(h, LWSSSCS_CONNECTED);
 			if (r != LWSSSSRET_OK)
 				return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
@@ -722,6 +734,12 @@ malformed:
 		     h->being_serialized && (
 				!strcmp(h->policy->u.http.method, "PUT") ||
 				!strcmp(h->policy->u.http.method, "POST"))) {
+#if defined(LWS_WITH_SYS_METRICS)
+			/*
+			 * If any hanging caliper measurement, dump it, and free any tags
+			 */
+			lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
 			r = lws_ss_event_helper(h, LWSSSCS_CONNECTED);
 			if (r)
 				return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
@@ -773,7 +791,7 @@ malformed:
 		return 0; /* don't passthru */
 
 	case LWS_CALLBACK_COMPLETED_CLIENT_HTTP:
-		lwsl_debug("%s: LWS_CALLBACK_COMPLETED_CLIENT_HTTP\n", __func__);
+		// lwsl_debug("%s: LWS_CALLBACK_COMPLETED_CLIENT_HTTP\n", __func__);
 
 		if (!h)
 			return -1;
@@ -919,7 +937,7 @@ malformed:
 		}
 
 #if defined(LWS_WITH_SERVER)
-		if (!(h->info.flags & LWSSSINFLAGS_ACCEPTED) &&
+		if ((h->info.flags & LWSSSINFLAGS_ACCEPTED) /* server */ &&
 		    (f & LWSSS_FLAG_EOM) &&
 		     lws_http_transaction_completed(wsi))
 			return -1;
@@ -973,6 +991,12 @@ malformed:
 		}
 
 		if (!h->ss_dangling_connected) {
+#if defined(LWS_WITH_SYS_METRICS)
+			/*
+			 * If any hanging caliper measurement, dump it, and free any tags
+			 */
+			lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
 			r = lws_ss_event_helper(h, LWSSSCS_CONNECTED);
 			if (r)
 				return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
diff --git a/lib/secure-streams/protocols/ss-mqtt.c b/lib/secure-streams/protocols/ss-mqtt.c
index 1e1d1cdb45641cabcaa87dbb32f2d4f69413fc62..0d719d568adcce6c79d3e530dda5efe802bc6312 100644
--- a/lib/secure-streams/protocols/ss-mqtt.c
+++ b/lib/secure-streams/protocols/ss-mqtt.c
@@ -94,6 +94,12 @@ secstream_mqtt(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 		h->retry = 0;
 		h->seqstate = SSSEQ_CONNECTED;
 		lws_sul_cancel(&h->sul);
+#if defined(LWS_WITH_SYS_METRICS)
+		/*
+		 * If any hanging caliper measurement, dump it, and free any tags
+		 */
+		lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
 		r = lws_ss_event_helper(h, LWSSSCS_CONNECTED);
 		if (r != LWSSSSRET_OK)
 			return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
diff --git a/lib/secure-streams/protocols/ss-raw.c b/lib/secure-streams/protocols/ss-raw.c
index aa020a233fa875ad669749bcf97926f0138d4089..69d96bbf58eb11b70c76f7603e55e9b8ea726161 100644
--- a/lib/secure-streams/protocols/ss-raw.c
+++ b/lib/secure-streams/protocols/ss-raw.c
@@ -60,8 +60,7 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 		if (!h)
 			break;
 		lws_sul_cancel(&h->sul_timeout);
-		lwsl_info("%s: %s, %s LWS_CALLBACK_CLOSED_CLIENT_HTTP\n",
-			  __func__, lws_ss_tag(h),
+		lwsl_info("%s: %s, %s RAW_CLOSE\n", __func__, lws_ss_tag(h),
 			  h->policy ? h->policy->streamtype : "no policy");
 		h->wsi = NULL;
 #if defined(LWS_WITH_SERVER)
@@ -69,6 +68,12 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 		lws_dll2_remove(&h->cli_list);
 		lws_pt_unlock(pt);
 #endif
+
+		/* wsi is going down anyway */
+		r = lws_ss_event_helper(h, LWSSSCS_DISCONNECTED);
+		if (r == LWSSSSRET_DESTROY_ME)
+			return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
+
 		if (h->policy && !(h->policy->flags & LWSSSPOLF_OPPORTUNISTIC) &&
 #if defined(LWS_WITH_SERVER)
 			    !(h->info.flags & LWSSSINFLAGS_ACCEPTED) && /* not server */
@@ -79,10 +84,7 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 				return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
 			break;
 		}
-		/* wsi is going down anyway */
-		r = lws_ss_event_helper(h, LWSSSCS_DISCONNECTED);
-		if (r == LWSSSSRET_DESTROY_ME)
-			return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
+
 		break;
 
 	case LWS_CALLBACK_RAW_CONNECTED:
@@ -91,6 +93,12 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 		h->retry = 0;
 		h->seqstate = SSSEQ_CONNECTED;
 		lws_sul_cancel(&h->sul);
+#if defined(LWS_WITH_SYS_METRICS)
+		/*
+		 * If any hanging caliper measurement, dump it, and free any tags
+		 */
+		lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
 		r = lws_ss_event_helper(h, LWSSSCS_CONNECTED);
 		if (r != LWSSSSRET_OK)
 			return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
diff --git a/lib/secure-streams/protocols/ss-ws.c b/lib/secure-streams/protocols/ss-ws.c
index fb25a28a06b03570259e3643ecaff98e0dc8d12e..ad24c40c60dabe8820a94a6ec279495b40862c98 100644
--- a/lib/secure-streams/protocols/ss-ws.c
+++ b/lib/secure-streams/protocols/ss-ws.c
@@ -106,6 +106,12 @@ secstream_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user,
 		h->retry = 0;
 		h->seqstate = SSSEQ_CONNECTED;
 		lws_sul_cancel(&h->sul);
+#if defined(LWS_WITH_SYS_METRICS)
+		/*
+		 * If any hanging caliper measurement, dump it, and free any tags
+		 */
+		lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
 		r = lws_ss_event_helper(h, LWSSSCS_CONNECTED);
 		if (r != LWSSSSRET_OK)
 			return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h);
diff --git a/lib/secure-streams/secure-streams-client.c b/lib/secure-streams/secure-streams-client.c
index e0477553ef16cfb7f244b90737bda5e6c283eea5..21bcade3c4e98d95bc0c4bb60f8843d6dbdc4a17 100644
--- a/lib/secure-streams/secure-streams-client.c
+++ b/lib/secure-streams/secure-streams-client.c
@@ -71,9 +71,23 @@ lws_sspc_sul_retry_cb(lws_sorted_usec_list_t *sul)
 	i.pwsi = &h->cwsi;
 	i.opaque_user_data = (void *)h;
 	i.ssl_connection = LCCSCF_SECSTREAM_PROXY_LINK;
+
+	lws_metrics_caliper_bind(h->cal_txn, h->context->mt_ss_cliprox_conn);
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metrics_tag_add(&h->cal_txn.mtags_owner, "ss", h->ssi.streamtype);
+#endif
+
 	/* this wsi is the link to the proxy */
 
 	if (!lws_client_connect_via_info(&i)) {
+
+#if defined(LWS_WITH_SYS_METRICS)
+		/*
+		 * If any hanging caliper measurement, dump it, and free any tags
+		 */
+		lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
+
 		lws_sul_schedule(h->context, 0, &h->sul_retry,
 				 lws_sspc_sul_retry_cb, LWS_US_PER_SEC);
 
@@ -147,6 +161,12 @@ callback_sspc_client(struct lws *wsi, enum lws_callback_reasons reason,
 
 	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
 		lwsl_warn("%s: CONNECTION_ERROR\n", __func__);
+#if defined(LWS_WITH_SYS_METRICS)
+		/*
+		 * If any hanging caliper measurement, dump it, and free any tags
+		 */
+		lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
 		lws_set_opaque_user_data(wsi, NULL);
 		h->cwsi = NULL;
 		lws_sul_schedule(h->context, 0, &h->sul_retry,
@@ -600,6 +620,13 @@ lws_sspc_destroy(lws_sspc_handle_t **ph)
 
 	h->destroying = 1;
 
+	/* if this caliper is still dangling at destroy, we failed */
+#if defined(LWS_WITH_SYS_METRICS)
+	/*
+	 * If any hanging caliper measurement, dump it, and free any tags
+	 */
+	lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
 	if (h->ss_dangling_connected && h->ssi.state) {
 		lws_sspc_event_helper(h, LWSSSCS_DISCONNECTED, 0);
 		h->ss_dangling_connected = 0;
diff --git a/lib/secure-streams/secure-streams-process.c b/lib/secure-streams/secure-streams-process.c
index 306d11d39bf097ea2e6ce5467c0f60d1a2a9c6a4..6bfa5d6df407ba2815a7b16e59736a688c569641 100644
--- a/lib/secure-streams/secure-streams-process.c
+++ b/lib/secure-streams/secure-streams-process.c
@@ -51,26 +51,6 @@
 
 #include <private-lib-core.h>
 
-/*
- * Because both sides of the connection share the conn, we allocate it
- * during accepted adoption, and both sides point to it.
- *
- * When .ss or .wsi close, they must NULL their entry here so no dangling
- * refereneces.
- *
- * The last one of the accepted side and the onward side to close frees it.
- */
-
-struct conn {
-	struct lws_ss_serialization_parser parser;
-
-	lws_dsh_t		*dsh;	/* unified buffer for both sides */
-	struct lws		*wsi;	/* the proxy's client side */
-	lws_ss_handle_t		*ss;	/* the onward, ss side */
-
-	lws_ss_conn_states_t	state;
-};
-
 struct raw_pss {
 	struct conn		*conn;
 };
@@ -313,9 +293,6 @@ callback_ss_proxy(struct lws *wsi, enum lws_callback_reasons reason,
 	lws_ss_metadata_t *md;
 	lws_ss_info_t ssi;
 	const uint8_t *cp;
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	lws_usec_t us;
-#endif
 	char s[256];
 	uint8_t *p;
 	size_t si;
@@ -588,7 +565,7 @@ callback_ss_proxy(struct lws *wsi, enum lws_callback_reasons reason,
 
 			cp = p;
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
+#if 0
 			if (cp[0] == LWSSS_SER_RXPRE_RX_PAYLOAD &&
 			    wsi->a.context->detailed_latency_cb) {
 
diff --git a/lib/secure-streams/secure-streams-serialize.c b/lib/secure-streams/secure-streams-serialize.c
index a9e7469bd236f247039c15a36b06a395c5c97549..0cbf088214340202381aa4f85f6097330e5c2369 100644
--- a/lib/secure-streams/secure-streams-serialize.c
+++ b/lib/secure-streams/secure-streams-serialize.c
@@ -224,7 +224,7 @@ lws_ss_deserialize_tx_payload(struct lws_dsh *dsh, struct lws *wsi,
 
 	*flags = (int)lws_ser_ru32be(&p[3]);
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
+#if 0
 	if (wsi && wsi->a.context->detailed_latency_cb) {
 		/*
 		 * use the proxied latency information to compute the client
@@ -725,7 +725,7 @@ payload_ff:
 					}
 				}
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
+#if 0
 				if (lws_det_lat_active(context)) {
 					lws_detlat_t d;
 
@@ -1233,6 +1233,13 @@ payload_ff:
 			 * CREATING now so we'll know the metadata to sync.
 			 */
 
+#if defined(LWS_WITH_SYS_METRICS)
+			/*
+			 * If any hanging caliper measurement, dump it, and free any tags
+			 */
+			lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
+
 			if (!h->creating_cb_done) {
 				if (lws_ss_check_next_state(&h->lc, &h->prev_ss_state,
 							    LWSSSCS_CREATING))
diff --git a/lib/secure-streams/secure-streams.c b/lib/secure-streams/secure-streams.c
index 1cc7e63435a45cd0e07eff21d4298d66dd7630a5..3d5f58f5efc26275c71ee59fb4e9a7c224f57f69 100644
--- a/lib/secure-streams/secure-streams.c
+++ b/lib/secure-streams/secure-streams.c
@@ -159,6 +159,7 @@ static const uint32_t ss_state_txn_validity[] = {
 					  (1 << LWSSSCS_POLL) |
 					  (1 << LWSSSCS_TIMEOUT) |
 					  (1 << LWSSSCS_DISCONNECTED) |
+					  (1 << LWSSSCS_UNREACHABLE) |
 					  (1 << LWSSSCS_DESTROYING),
 
 	[LWSSSCS_SERVER_TXN]		= (1 << LWSSSCS_DISCONNECTED) |
@@ -671,6 +672,14 @@ _lws_ss_client_connect(lws_ss_handle_t *h, int is_retry, void *conn_if_sspc_onw)
 	lwsl_info("%s: connecting %s, '%s' '%s' %s\n", __func__, i.method,
 			i.alpn, i.address, i.path);
 
+#if defined(LWS_WITH_SYS_METRICS)
+	/* possibly already hanging connect retry... */
+	if (!h->cal_txn.mt)
+		lws_metrics_caliper_bind(h->cal_txn, h->context->mth_ss_conn);
+
+	lws_metrics_tag_add(&h->cal_txn.mtags_owner, "ss", h->policy->streamtype);
+#endif
+
 	h->txn_ok = 0;
 	r = lws_ss_event_helper(h, LWSSSCS_CONNECTING);
 	if (r) {
@@ -1181,6 +1190,13 @@ lws_ss_destroy(lws_ss_handle_t **ppss)
 	lws_fi_destroy(&h->fi);
 #endif
 
+#if defined(LWS_WITH_SYS_METRICS)
+	/*
+	 * If any hanging caliper measurement, dump it, and free any tags
+	 */
+	lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL);
+#endif
+
 	lws_sul_cancel(&h->sul_timeout);
 
 	/* confirm no sul left scheduled in handle or user allocation object */
diff --git a/lib/system/CMakeLists.txt b/lib/system/CMakeLists.txt
index 9ad9d70948205b240645d27be6a50347b1a7d32d..654264b4afcb45edbb504160adb745a7c895fa4f 100644
--- a/lib/system/CMakeLists.txt
+++ b/lib/system/CMakeLists.txt
@@ -63,6 +63,8 @@ if (LWS_WITH_NETWORK)
 			system/fault-injection/fault-injection.c)
 	endif()
 
+	add_subdir_include_dirs(metrics)
+
 endif()
 
 #
diff --git a/lib/system/async-dns/async-dns-parse.c b/lib/system/async-dns/async-dns-parse.c
index c0dd9ae1aab96b4debb2855e4a690ef2c4916092..001455550f3a65a310550f58da36e20d27c975b0 100644
--- a/lib/system/async-dns/async-dns-parse.c
+++ b/lib/system/async-dns/async-dns-parse.c
@@ -1,7 +1,7 @@
 /*
  * libwebsockets - small server side websockets and web server implementation
  *
- * Copyright (C) 2010 - 2019 Andy Green <andy@warmcat.com>
+ * Copyright (C) 2010 - 2021 Andy Green <andy@warmcat.com>
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to
@@ -690,6 +690,8 @@ lws_adns_parse_udp(lws_async_dns_t *dns, const uint8_t *pkt, size_t len)
 	c->incomplete = 0;
 	lws_async_dns_complete(q, q->firstcache);
 
+	q->go_nogo = METRES_GO;
+
 	/*
 	 * the query is completely finished with
 	 */
diff --git a/lib/system/async-dns/async-dns.c b/lib/system/async-dns/async-dns.c
index 429d85aeb1a2870f2da3c75c17b3c225b4ee72a3..ed13fd740495de8654fef40598ddc34805aaafa8 100644
--- a/lib/system/async-dns/async-dns.c
+++ b/lib/system/async-dns/async-dns.c
@@ -34,6 +34,8 @@ static const lws_retry_bo_t retry_policy = {
 void
 lws_adns_q_destroy(lws_adns_q_t *q)
 {
+	lws_metrics_caliper_report(q->metcal, (char)q->go_nogo);
+
 	lws_sul_cancel(&q->sul);
 	lws_sul_cancel(&q->write_sul);
 	lws_dll2_remove(&q->list);
@@ -703,6 +705,10 @@ lws_async_dns_query(struct lws_context *context, int tsi, const char *name,
 		if (c->results)
 			c->refcount++;
 
+#if defined(LWS_WITH_SYS_METRICS)
+		lws_metric_event(context->mt_adns_cache,  METRES_GO, 0);
+#endif
+
 		if (cb(wsi, name, c->results, m, opaque) == NULL)
 			return LADNS_RET_FAILED_WSI_CLOSED;
 
@@ -710,6 +716,10 @@ lws_async_dns_query(struct lws_context *context, int tsi, const char *name,
 	} else
 		lwsl_info("%s: %s uncached\n", __func__, name);
 
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metric_event(context->mt_adns_cache, METRES_NOGO, 0);
+#endif
+
 	/*
 	 * It's a 1.2.3.4 or ::1 type IP address already?  We don't need a dns
 	 * server set up to be able to create an addrinfo result for that.
@@ -876,6 +886,10 @@ lws_async_dns_query(struct lws_context *context, int tsi, const char *name,
 
 	lws_dll2_add_head(&q->list, &dns->waiting);
 
+	lws_metrics_caliper_bind(q->metcal, context->mt_conn_dns);
+	q->go_nogo = METRES_NOGO;
+	/* caliper is reported in lws_adns_q_destroy */
+
 	lwsl_info("%s: created new query: %s\n", __func__, name);
 	lws_adns_dump(dns);
 
diff --git a/lib/system/async-dns/private-lib-async-dns.h b/lib/system/async-dns/private-lib-async-dns.h
index 969833be8d967c1582a5712d2044691e954d3673..f213448ab260f6176d7f02b2e8074a1fe0084c5a 100644
--- a/lib/system/async-dns/private-lib-async-dns.h
+++ b/lib/system/async-dns/private-lib-async-dns.h
@@ -58,6 +58,8 @@ typedef struct {
 	lws_sorted_usec_list_t	write_sul;	/* fail if unable to write by this time */
 	lws_dll2_t		list;
 
+	lws_metrics_caliper_compose(metcal)
+
 	lws_dll2_owner_t	wsi_adns;
 	lws_async_dns_cb_t	standalone_cb;	/* if not associated to wsi */
 	struct lws_context	*context;
@@ -83,6 +85,7 @@ typedef struct {
 
 	uint8_t			recursion;
 	uint8_t			tids;
+	uint8_t			go_nogo;
 
 	uint8_t			is_retry:1;
 
diff --git a/lib/system/metrics/CMakeLists.txt b/lib/system/metrics/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3ed7f3f3c13d95ee8bffe9bb87a0453488feb348
--- /dev/null
+++ b/lib/system/metrics/CMakeLists.txt
@@ -0,0 +1,10 @@
+include_directories(.)
+
+if (LWS_WITH_SYS_METRICS)
+	list(APPEND SOURCES
+		system/metrics/metrics.c
+	)
+endif()
+
+exports_to_parent_scope()
+
diff --git a/lib/system/metrics/metrics.c b/lib/system/metrics/metrics.c
new file mode 100644
index 0000000000000000000000000000000000000000..95a599f3cc3de4fd653dd94f6a0b3851090defd8
--- /dev/null
+++ b/lib/system/metrics/metrics.c
@@ -0,0 +1,891 @@
+/*
+ * lws Generic Metrics
+ *
+ * Copyright (C) 2019 - 2021 Andy Green <andy@warmcat.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "private-lib-core.h"
+#include <assert.h>
+
+int
+lws_metrics_tag_add(lws_dll2_owner_t *owner, const char *name, const char *val)
+{
+	size_t vl = strlen(val);
+	lws_metrics_tag_t *tag;
+
+	// lwsl_notice("%s: adding %s=%s\n", __func__, name, val);
+
+	/*
+	 * Remove (in order to replace) any existing tag of same name
+	 */
+
+	lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) {
+		tag = lws_container_of(d, lws_metrics_tag_t, list);
+
+		if (!strcmp(name, tag->name)) {
+			lws_dll2_remove(&tag->list);
+			lws_free(tag);
+			break;
+		}
+
+	} lws_end_foreach_dll(d);
+
+	/*
+	 * Create the new tag
+	 */
+
+	tag = lws_malloc(sizeof(*tag) + vl + 1, __func__);
+	if (!tag)
+		return 1;
+
+	lws_dll2_clear(&tag->list);
+	tag->name = name;
+	memcpy(&tag[1], val, vl + 1);
+
+	lws_dll2_add_tail(&tag->list, owner);
+
+	return 0;
+}
+
+int
+lws_metrics_tag_wsi_add(struct lws *wsi, const char *name, const char *val)
+{
+	__lws_lc_tag(NULL, &wsi->lc, "|%s", val);
+
+	return lws_metrics_tag_add(&wsi->cal_conn.mtags_owner, name, val);
+}
+
+#if defined(LWS_WITH_SECURE_STREAMS)
+int
+lws_metrics_tag_ss_add(struct lws_ss_handle *ss, const char *name, const char *val)
+{
+	__lws_lc_tag(NULL, &ss->lc, "|%s", val);
+	return lws_metrics_tag_add(&ss->cal_txn.mtags_owner, name, val);
+}
+#if defined(LWS_WITH_SECURE_STREAMS)
+int
+lws_metrics_tag_sspc_add(struct lws_sspc_handle *sspc, const char *name,
+			 const char *val)
+{
+	__lws_lc_tag(NULL, &sspc->lc, "|%s", val);
+	return lws_metrics_tag_add(&sspc->cal_txn.mtags_owner, name, val);
+}
+#endif
+#endif
+
+void
+lws_metrics_tags_destroy(lws_dll2_owner_t *owner)
+{
+	lws_metrics_tag_t *t;
+
+	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, owner->head) {
+		t = lws_container_of(d, lws_metrics_tag_t, list);
+
+		lws_dll2_remove(&t->list);
+		lws_free(t);
+
+	} lws_end_foreach_dll_safe(d, d1);
+}
+
+size_t
+lws_metrics_tags_serialize(lws_dll2_owner_t *owner, char *buf, size_t len)
+{
+	char *end = buf + len - 1, *p = buf;
+	lws_metrics_tag_t *t;
+
+	lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) {
+		t = lws_container_of(d, lws_metrics_tag_t, list);
+
+		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+				  "%s=\"%s\"", t->name, (const char *)&t[1]);
+
+		if (d->next && p + 2 < end)
+			*p++ = ',';
+
+	} lws_end_foreach_dll(d);
+
+	*p = '\0';
+
+	return lws_ptr_diff_size_t(p, buf);
+}
+
+const char *
+lws_metrics_tag_get(lws_dll2_owner_t *owner, const char *name)
+{
+	lws_metrics_tag_t *t;
+
+	lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) {
+		t = lws_container_of(d, lws_metrics_tag_t, list);
+
+		if (!strcmp(name, t->name))
+			return (const char *)&t[1];
+
+	} lws_end_foreach_dll(d);
+
+	return NULL;
+}
+
+static int
+lws_metrics_dump_cb(lws_metric_pub_t *pub, void *user);
+
+static void
+lws_metrics_report_and_maybe_clear(struct lws_context *ctx, lws_metric_pub_t *pub)
+{
+	if (!pub->us_first || pub->us_last == pub->us_dumped)
+		return;
+
+	lws_metrics_dump_cb(pub, ctx);
+}
+
+static void
+lws_metrics_periodic_cb(lws_sorted_usec_list_t *sul)
+{
+	lws_metric_policy_dyn_t *dmp = lws_container_of(sul,
+						lws_metric_policy_dyn_t, sul);
+	struct lws_context *ctx = lws_container_of(dmp->list.owner,
+					struct lws_context, owner_mtr_dynpol);
+
+	if (!ctx->system_ops || !ctx->system_ops->metric_report)
+		return;
+
+	lws_start_foreach_dll(struct lws_dll2 *, d, dmp->owner.head) {
+		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
+		lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt);
+
+		lws_metrics_report_and_maybe_clear(ctx, pub);
+
+	} lws_end_foreach_dll(d);
+
+#if defined(LWS_WITH_SYS_SMD) && defined(LWS_WITH_SECURE_STREAMS)
+	(void)lws_smd_msg_printf(ctx, LWSSMDCL_METRICS,
+				 "{\"dump\":\"%s\",\"ts\":%lu}",
+				   dmp->policy->name,
+				   (long)ctx->last_policy);
+#endif
+
+	if (dmp->policy->us_schedule)
+		lws_sul_schedule(ctx, 0, &dmp->sul,
+				 lws_metrics_periodic_cb,
+				 dmp->policy->us_schedule);
+}
+
+/*
+ * Policies are in two pieces, a const policy and a dynamic part that contains
+ * lists and sul timers for the policy etc.  This creates a dynmic part
+ * corresponding to the static part.
+ *
+ * Metrics can exist detached from being bound to any policy about how to
+ * report them, these are collected but not reported unless they later become
+ * bound to a reporting policy dynamically.
+ */
+
+lws_metric_policy_dyn_t *
+lws_metrics_policy_dyn_create(struct lws_context *ctx,
+			      const lws_metric_policy_t *po)
+{
+	lws_metric_policy_dyn_t *dmet;
+
+	dmet = lws_zalloc(sizeof(*dmet), __func__);
+	if (!dmet)
+		return NULL;
+
+	dmet->policy = po;
+	lws_dll2_add_tail(&dmet->list, &ctx->owner_mtr_dynpol);
+
+	if (po->us_schedule)
+		lws_sul_schedule(ctx, 0, &dmet->sul,
+				 lws_metrics_periodic_cb,
+				 po->us_schedule);
+
+	return dmet;
+}
+
+/*
+ * Get a dynamic metrics policy from the const one, may return NULL if OOM
+ */
+
+lws_metric_policy_dyn_t *
+lws_metrics_policy_get_dyn(struct lws_context *ctx,
+			   const lws_metric_policy_t *po)
+{
+	lws_start_foreach_dll(struct lws_dll2 *, d, ctx->owner_mtr_dynpol.head) {
+		lws_metric_policy_dyn_t *dm =
+			lws_container_of(d, lws_metric_policy_dyn_t, list);
+
+		if (dm->policy == po)
+			return dm;
+
+	} lws_end_foreach_dll(d);
+
+	/*
+	 * no dyn policy part for this const policy --> create one
+	 *
+	 * We want a dynamic part for listing metrics that bound to the policy
+	 */
+
+	return lws_metrics_policy_dyn_create(ctx, po);
+}
+
+static int
+lws_metrics_check_in_policy(const char *polstring, const char *name)
+{
+	struct lws_tokenize ts;
+
+	memset(&ts, 0, sizeof(ts));
+
+	ts.start = polstring;
+	ts.len = strlen(polstring);
+	ts.flags = (uint16_t)(LWS_TOKENIZE_F_MINUS_NONTERM |
+			      LWS_TOKENIZE_F_ASTERISK_NONTERM |
+			      LWS_TOKENIZE_F_COMMA_SEP_LIST |
+			      LWS_TOKENIZE_F_NO_FLOATS |
+			      LWS_TOKENIZE_F_DOT_NONTERM);
+
+	do {
+		ts.e = (int8_t)lws_tokenize(&ts);
+
+		if (ts.e == LWS_TOKZE_TOKEN) {
+			if (!lws_strcmp_wildcard(ts.token, ts.token_len, name))
+				/* yes, we are mentioned in this guy's policy */
+				return 0;
+		}
+	} while (ts.e > 0);
+
+	/* no, this policy doesn't apply to a metric with our name */
+
+	return 1;
+}
+
+static const lws_metric_policy_t *
+lws_metrics_find_policy(struct lws_context *ctx, const char *name)
+{
+	const lws_metric_policy_t *mp = ctx->metrics_policies;
+
+	if (!mp) {
+#if defined(LWS_WITH_SECURE_STREAMS)
+		if (ctx->pss_policies)
+			mp = ctx->pss_policies->metrics;
+#endif
+		if (!mp)
+			return NULL;
+	}
+
+	while (mp) {
+		if (mp->report && !lws_metrics_check_in_policy(mp->report, name))
+			return mp;
+
+		mp = mp->next;
+	}
+
+	return NULL;
+}
+
+/*
+ * Create a lws_metric_t, bind to a named policy if possible (or add to the
+ * context list of unbound metrics) and set its lws_system
+ * idx.  The metrics objects themselves are typically composed into other
+ * objects and are well-known composed members of them.
+ */
+
+lws_metric_t *
+lws_metric_create(struct lws_context *ctx, uint8_t flags, const char *name)
+{
+	const lws_metric_policy_t *po;
+	lws_metric_policy_dyn_t *dmp;
+	lws_metric_pub_t *pub;
+	lws_metric_t *mt;
+	char pname[32];
+	size_t nl;
+
+	if (ctx->metrics_prefix) {
+
+		/*
+		 * In multi-process case, we want to prefix metrics from this
+		 * process / context with a string distinguishing which
+		 * application they came from
+		 */
+
+		nl = (size_t)lws_snprintf(pname, sizeof(pname) - 1, "%s.%s",
+				  ctx->metrics_prefix, name);
+		name = pname;
+	} else
+		nl = strlen(name);
+
+	mt = (lws_metric_t *)lws_zalloc(sizeof(*mt) /* private */ +
+					sizeof(lws_metric_pub_t) +
+					nl + 1 /* copy of metric name */,
+					__func__);
+	if (!mt)
+		return NULL;
+
+	pub = lws_metrics_priv_to_pub(mt);
+	pub->name = (char *)pub + sizeof(lws_metric_pub_t);
+	memcpy((char *)pub->name, name, nl + 1);
+	pub->flags = flags;
+
+	/* after these common members, we have to use the right type */
+
+	if (!(flags & LWSMTFL_REPORT_HIST)) {
+		/* anything is smaller or equal to this */
+		pub->u.agg.min = ~(u_mt_t)0;
+		pub->us_first = lws_now_usecs();
+	}
+
+	mt->ctx = ctx;
+
+	/*
+	 * Let's see if we can bind to a reporting policy straight away
+	 */
+
+	po = lws_metrics_find_policy(ctx, name);
+	if (po) {
+		dmp = lws_metrics_policy_get_dyn(ctx, po);
+		if (dmp) {
+			lwsl_notice("%s: metpol %s\n", __func__, name);
+			lws_dll2_add_tail(&mt->list, &dmp->owner);
+
+			return 0;
+		}
+	}
+
+	/*
+	 * If not, well, let's go on without and maybe later at runtime, he'll
+	 * get interested in us and apply a reporting policy
+	 */
+
+	lws_dll2_add_tail(&mt->list, &ctx->owner_mtr_no_pol);
+
+	return mt;
+}
+
+/*
+ * If our metric is bound to a reporting policy, return a pointer to it,
+ * otherwise NULL
+ */
+
+const lws_metric_policy_t *
+lws_metric_get_policy(lws_metric_t *mt)
+{
+	lws_metric_policy_dyn_t *dp;
+
+	/*
+	 * Our metric must either be on the "no policy" context list or
+	 * listed by the dynamic part of the policy it is bound to
+	 */
+	assert(mt->list.owner);
+
+	if ((char *)mt->list.owner >= (char *)mt->ctx &&
+	    (char *)mt->list.owner < (char *)mt->ctx + sizeof(struct lws_context))
+		/* we are on the "no policy" context list */
+		return NULL;
+
+	/* we are listed by a dynamic policy owner */
+
+	dp = lws_container_of(mt->list.owner, lws_metric_policy_dyn_t, owner);
+
+	/* return the const policy the dynamic policy represents */
+
+	return dp->policy;
+}
+
+void
+lws_metric_rebind_policies(struct lws_context *ctx)
+{
+	const lws_metric_policy_t *po;
+	lws_metric_policy_dyn_t *dmp;
+
+	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
+				   ctx->owner_mtr_no_pol.head) {
+		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
+		lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt);
+
+		po = lws_metrics_find_policy(ctx, pub->name);
+		if (po) {
+			dmp = lws_metrics_policy_get_dyn(ctx, po);
+			if (dmp) {
+				lwsl_info("%s: %s <- pol %s\n", __func__,
+						pub->name, po->name);
+				lws_dll2_remove(&mt->list);
+				lws_dll2_add_tail(&mt->list, &dmp->owner);
+			}
+		} else
+			lwsl_debug("%s: no pol for %s\n", __func__, pub->name);
+
+	} lws_end_foreach_dll_safe(d, d1);
+}
+
+int
+lws_metric_destroy(lws_metric_t **pmt, int keep)
+{
+	lws_metric_t *mt = *pmt;
+	lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt);
+
+	if (!mt)
+		return 0;
+
+	lws_dll2_remove(&mt->list);
+
+	if (keep) {
+		lws_dll2_add_tail(&mt->list, &mt->ctx->owner_mtr_no_pol);
+
+		return 0;
+	}
+
+	if (pub->flags & LWSMTFL_REPORT_HIST) {
+		lws_metric_bucket_t *b = pub->u.hist.head, *b1;
+
+		pub->u.hist.head = NULL;
+
+		while (b) {
+			b1 = b->next;
+			lws_free(b);
+			b = b1;
+		}
+	}
+
+	lws_free(mt);
+	*pmt = NULL;
+
+	return 0;
+}
+
+/*
+ * Allow an existing metric to have its reporting policy changed at runtime
+ */
+
+int
+lws_metric_switch_policy(lws_metric_t *mt, const char *polname)
+{
+	const lws_metric_policy_t *po;
+	lws_metric_policy_dyn_t *dmp;
+
+	po = lws_metrics_find_policy(mt->ctx, polname);
+	if (!po)
+		return 1;
+
+	dmp = lws_metrics_policy_get_dyn(mt->ctx, po);
+	if (!dmp)
+		return 1;
+
+	lws_dll2_remove(&mt->list);
+	lws_dll2_add_tail(&mt->list, &dmp->owner);
+
+	return 0;
+}
+
+/*
+ * If keep is set, don't destroy existing metrics objects, just detach them
+ * from the policy being deleted and keep track of them on ctx->
+ * owner_mtr_no_pol
+ */
+
+void
+lws_metric_policy_dyn_destroy(lws_metric_policy_dyn_t *dm, int keep)
+{
+	lws_sul_cancel(&dm->sul);
+
+	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, dm->owner.head) {
+		lws_metric_t *m = lws_container_of(d, lws_metric_t, list);
+
+		lws_metric_destroy(&m, keep);
+
+	} lws_end_foreach_dll_safe(d, d1);
+
+	lws_sul_cancel(&dm->sul);
+
+	lws_dll2_remove(&dm->list);
+	lws_free(dm);
+}
+
+/*
+ * Destroy all dynamic metrics policies, deinit any metrics still using them
+ */
+
+void
+lws_metrics_destroy(struct lws_context *ctx)
+{
+	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
+				   ctx->owner_mtr_dynpol.head) {
+		lws_metric_policy_dyn_t *dm =
+			lws_container_of(d, lws_metric_policy_dyn_t, list);
+
+		lws_metric_policy_dyn_destroy(dm, 0); /* don't keep */
+
+	} lws_end_foreach_dll_safe(d, d1);
+
+	/* destroy metrics with no current policy too... */
+
+	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
+				   ctx->owner_mtr_no_pol.head) {
+		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
+
+		lws_metric_destroy(&mt, 0); /* don't keep */
+
+	} lws_end_foreach_dll_safe(d, d1);
+
+	/* ... that's the whole allocated metrics footprint gone... */
+}
+
+int
+lws_metrics_hist_bump_(lws_metric_pub_t *pub, const char *name)
+{
+	lws_metric_bucket_t *buck = pub->u.hist.head;
+	size_t nl = strlen(name);
+	char *nm;
+
+	if (!(pub->flags & LWSMTFL_REPORT_HIST)) {
+		lwsl_err("%s: %s not histogram: flags %d\n", __func__,
+				pub->name, pub->flags);
+		assert(0);
+	}
+	assert(nl < 255);
+
+	pub->us_last = lws_now_usecs();
+	if (!pub->us_first)
+		pub->us_first = pub->us_last;
+
+	while (buck) {
+		if (lws_metric_bucket_name_len(buck) == nl &&
+		    !strcmp(name, lws_metric_bucket_name(buck))) {
+			buck->count++;
+			goto happy;
+		}
+		buck = buck->next;
+	}
+
+	buck = lws_malloc(sizeof(*buck) + nl + 2, __func__);
+	if (!buck)
+		return 1;
+
+	nm = (char *)buck + sizeof(*buck);
+	/* length byte at beginning of name, avoid struct alignment overhead */
+	*nm = (char)nl;
+	memcpy(nm + 1, name, nl + 1);
+
+	buck->next = pub->u.hist.head;
+	pub->u.hist.head = buck;
+	buck->count = 1;
+	pub->u.hist.list_size++;
+
+happy:
+	pub->u.hist.total_count++;
+
+	return 0;
+}
+
+int
+lws_metrics_hist_bump_describe_wsi(struct lws *wsi, lws_metric_pub_t *pub,
+				   const char *name)
+{
+	char desc[192], d1[48], *p = desc, *end = desc + sizeof(desc);
+
+#if defined(LWS_WITH_SECURE_STREAMS)
+#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API)
+	if (wsi->client_bound_sspc) {
+		lws_sspc_handle_t *h = (lws_sspc_handle_t *)wsi->a.opaque_user_data;
+		if (h)
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",",
+				  h->ssi.streamtype);
+	} else
+		if (wsi->client_proxy_onward) {
+			struct conn *conn = (struct conn *)wsi->a.opaque_user_data;
+
+			if (conn && conn->ss)
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",",
+				  conn->ss->info.streamtype);
+		} else
+#endif
+	if (wsi->for_ss) {
+		lws_ss_handle_t *h = (lws_ss_handle_t *)wsi->a.opaque_user_data;
+		if (h)
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",",
+				  h->info.streamtype);
+	}
+#endif
+
+#if defined(LWS_WITH_CLIENT)
+	if (wsi->stash && wsi->stash->cis[CIS_HOST])
+		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "hostname=\"%s\",",
+				wsi->stash->cis[CIS_HOST]);
+#endif
+
+	lws_sa46_write_numeric_address(&wsi->sa46_peer, d1, sizeof(d1));
+	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "peer=\"%s\",", d1);
+
+	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "%s", name);
+
+	lws_metrics_hist_bump_(pub, desc);
+
+	return 0;
+}
+
+int
+lws_metrics_foreach(struct lws_context *ctx, void *user,
+		    int (*cb)(lws_metric_pub_t *pub, void *user))
+{
+	int n;
+
+	lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1,
+				   ctx->owner_mtr_no_pol.head) {
+		lws_metric_t *mt = lws_container_of(d, lws_metric_t, list);
+
+		n = cb(lws_metrics_priv_to_pub(mt), user);
+		if (n)
+			return n;
+
+	} lws_end_foreach_dll_safe(d, d1);
+
+	lws_start_foreach_dll_safe(struct lws_dll2 *, d2, d3,
+				   ctx->owner_mtr_dynpol.head) {
+		lws_metric_policy_dyn_t *dm =
+			lws_container_of(d2, lws_metric_policy_dyn_t, list);
+
+		lws_start_foreach_dll_safe(struct lws_dll2 *, e, e1,
+					   dm->owner.head) {
+
+			lws_metric_t *mt = lws_container_of(e, lws_metric_t, list);
+
+			n = cb(lws_metrics_priv_to_pub(mt), user);
+			if (n)
+				return n;
+
+		} lws_end_foreach_dll_safe(e, e1);
+
+	} lws_end_foreach_dll_safe(d2, d3);
+
+	return 0;
+}
+
+static int
+lws_metrics_dump_cb(lws_metric_pub_t *pub, void *user)
+{
+	struct lws_context *ctx = (struct lws_context *)user;
+	int n;
+
+	if (!ctx->system_ops || !ctx->system_ops->metric_report)
+		return 0;
+
+	/*
+	 * return nonzero to reset stats
+	 */
+
+	n = ctx->system_ops->metric_report(pub);
+
+	/* track when we dumped it... */
+
+	pub->us_first = pub->us_dumped = lws_now_usecs();
+	pub->us_last = 0;
+
+	if (!n)
+		return 0;
+
+	/* ... and clear it back to 0 */
+
+	if (pub->flags & LWSMTFL_REPORT_HIST) {
+		lws_metric_bucket_t *b = pub->u.hist.head, *b1;
+		pub->u.hist.head = NULL;
+
+		while (b) {
+			b1 = b->next;
+			lws_free(b);
+			b = b1;
+		}
+		pub->u.hist.total_count = 0;
+		pub->u.hist.list_size = 0;
+	} else
+		memset(&pub->u.agg, 0, sizeof(pub->u.agg));
+
+	return 0;
+}
+
+void
+lws_metrics_dump(struct lws_context *ctx)
+{
+	lws_metrics_foreach(ctx, ctx, lws_metrics_dump_cb);
+}
+
+static int
+_lws_metrics_format(lws_metric_pub_t *pub, lws_usec_t now, int gng,
+		    char *buf, size_t len)
+{
+	const lws_humanize_unit_t *schema = humanize_schema_si;
+	char *end = buf + len - 1, *obuf = buf;
+
+	if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US)
+		schema = humanize_schema_us;
+
+	if (!(pub->flags & LWSMTFL_REPORT_MEAN)) {
+		/* only the sum is meaningful */
+		if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US) {
+
+			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), " %u, ",
+						(unsigned int)pub->u.agg.count[gng]);
+
+			buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
+					    (uint64_t)pub->u.agg.sum[gng],
+					    humanize_schema_us);
+
+			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), " / ");
+
+			buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
+					    (uint64_t)(now - pub->us_first),
+					    humanize_schema_us);
+
+			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
+					    " (%d%%)", (int)((100 * pub->u.agg.sum[gng]) /
+						(unsigned long)(now - pub->us_first)));
+		} else {
+			/* it's a monotonic ordinal, like total tx */
+			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "(%u) ",
+					(unsigned int)pub->u.agg.count[gng]);
+			buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
+					    (uint64_t)pub->u.agg.sum[gng],
+					    humanize_schema_si);
+		}
+
+	} else {
+		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%u, mean: ", (unsigned int)pub->u.agg.count[gng]);
+		/* the average over the period is meaningful */
+		buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf),
+				    (uint64_t)(pub->u.agg.count[gng] ?
+					 pub->u.agg.sum[gng] / pub->u.agg.count[gng] : 0),
+				    schema);
+	}
+
+	return lws_ptr_diff(buf, obuf);
+}
+
+int
+lws_metrics_format(lws_metric_pub_t *pub, lws_metric_bucket_t **sub, char *buf, size_t len)
+{
+	char *end = buf + len - 1, *obuf = buf;
+	lws_usec_t t = lws_now_usecs();
+	const lws_humanize_unit_t *schema = humanize_schema_si;
+
+	if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US)
+		schema = humanize_schema_us;
+
+	if (pub->flags & LWSMTFL_REPORT_HIST) {
+
+		if (*sub == NULL)
+			return 0;
+
+		if (*sub) {
+			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
+					    "%s{%s} %llu", pub->name,
+					    lws_metric_bucket_name(*sub),
+					    (unsigned long long)(*sub)->count);
+
+			*sub = (*sub)->next;
+		}
+
+		goto happy;
+	}
+
+	buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%s: ",
+				pub->name);
+
+	if (!pub->u.agg.count[METRES_GO] && !pub->u.agg.count[METRES_NOGO])
+		return 0;
+
+	if (pub->u.agg.count[METRES_GO]) {
+		if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO))
+			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
+					    "Go: ");
+		buf += _lws_metrics_format(pub, t, METRES_GO, buf,
+					   lws_ptr_diff_size_t(end, buf));
+	}
+
+	if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO) && pub->u.agg.count[METRES_NOGO]) {
+		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", NoGo: ");
+		buf += _lws_metrics_format(pub, t, METRES_NOGO, buf,
+					   lws_ptr_diff_size_t(end, buf));
+	}
+
+	if (pub->flags & LWSMTFL_REPORT_MEAN) {
+		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", min: ");
+		buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), pub->u.agg.min,
+				    schema);
+		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", max: ");
+		buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), pub->u.agg.max,
+				    schema);
+	}
+
+happy:
+	if (pub->flags & LWSMTFL_REPORT_HIST)
+		return 1;
+
+	*sub = NULL;
+
+	return lws_ptr_diff(buf, obuf);
+}
+
+/*
+ * We want to, at least internally, record an event... depending on the policy,
+ * that might cause us to call through to the lws_system apis, or just update
+ * our local stats about it and dump at the next periodic chance (also set by
+ * the policy)
+ */
+
+void
+lws_metric_event(lws_metric_t *mt, char go_nogo, u_mt_t val)
+{
+	lws_metric_pub_t *pub;
+
+	assert((go_nogo & 0xfe) == 0);
+
+	if (!mt)
+		return;
+
+	pub = lws_metrics_priv_to_pub(mt);
+	assert(!(pub->flags & LWSMTFL_REPORT_HIST));
+
+	pub->us_last = lws_now_usecs();
+	if (!pub->us_first)
+		pub->us_first = pub->us_last;
+	pub->u.agg.count[(int)go_nogo]++;
+	pub->u.agg.sum[(int)go_nogo] += val;
+	if (val > pub->u.agg.max)
+		pub->u.agg.max = val;
+	if (val < pub->u.agg.min)
+		pub->u.agg.min = val;
+
+	if (pub->flags & LWSMTFL_REPORT_OOB)
+		lws_metrics_report_and_maybe_clear(mt->ctx, pub);
+}
+
+
+void
+lws_metrics_hist_bump_priv_tagged(lws_metric_pub_t *mt, lws_dll2_owner_t *tow,
+				  lws_dll2_owner_t *tow2)
+{
+	char qual[192];
+	size_t p;
+
+	p = lws_metrics_tags_serialize(tow, qual, sizeof(qual));
+	if (tow2)
+		lws_metrics_tags_serialize(tow2, qual + p,
+				sizeof(qual) - p);
+
+	lwsl_warn("%s: '%s'\n", __func__, qual);
+
+	lws_metrics_hist_bump(mt, qual);
+}
diff --git a/lib/system/metrics/private-lib-system-metrics.h b/lib/system/metrics/private-lib-system-metrics.h
new file mode 100644
index 0000000000000000000000000000000000000000..07f4daf43a553430c0719996569d05bc1d937b6d
--- /dev/null
+++ b/lib/system/metrics/private-lib-system-metrics.h
@@ -0,0 +1,124 @@
+/*
+ * lws System Metrics
+ *
+ * Copyright (C) 2021 Andy Green <andy@warmcat.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/*
+ * Const struct that describes a policy for processing raw metrics to turn them
+ * into events.
+ *
+ * Typically although we want to monitor every event, the data produced can be
+ * too large, and many events that are "normal" just need to be counted as such;
+ * outliers or change-to-continuous outliers may deserve closer recording as
+ * events in their own right.
+ *
+ * Mean computation must "decay" as it ages, we do this by halving the sum and
+ * count after .us_decay_unit us.
+ *
+ * We don't acknowledge outliers until there are at least .min_contributors
+ * in the current mean (which is subject to decaying)
+ *
+ * We decide something is an outlier event if it deviates from the mean by
+ * .pc_outlier_deviation %.
+ */
+
+/*
+ * The dynamic counterpart for each static metric policy, this is on heap
+ * one per const lws_metric_policy_t.  It's listed in context->owner_mtr_dynpol
+ */
+
+typedef struct lws_metric_policy_dyn {
+	const lws_metric_policy_t	*policy;
+	/**< the static part of the policy we belong to... can be NULL if no
+	 * policy matches or the policy was invalidated */
+
+	lws_dll2_owner_t		owner;
+	/**< list of metrics that are using this policy */
+
+	lws_dll2_t			list;
+	/**< context owns us */
+
+	lws_sorted_usec_list_t		sul;
+	/**< schedule periodic reports for metrics using this policy */
+} lws_metric_policy_dyn_t;
+
+/*
+ * A metrics private part, encapsulating the public part
+ */
+
+typedef struct lws_metric {
+
+	lws_dll2_t			list;
+	/**< owned by either 1) ctx.lws_metric_policy_dyn_t.owner, or
+	 * 2) ctx.owner_mtr_no_pol */
+
+	struct lws_context		*ctx;
+
+	/* public part overallocated */
+} lws_metric_t;
+
+
+#if defined(LWS_WITH_SYS_METRICS)
+#define lws_metrics_hist_bump_priv(_mt, _name) \
+		lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_mt), _name)
+#define lws_metrics_hist_bump_priv_wsi(_wsi, _hist, _name) \
+		lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_wsi->a.context->_hist), _name)
+#define lws_metrics_hist_bump_priv_ss(_ss, _hist, _name) \
+		lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_ss->context->_hist), _name)
+#define lws_metrics_priv_to_pub(_x) ((lws_metric_pub_t *)&(_x)[1])
+#else
+#define lws_metrics_hist_bump_priv(_mt, _name)
+#define lws_metrics_hist_bump_priv_wsi(_wsi, _hist, _name)
+#define lws_metrics_hist_bump_priv_ss(_ss, _hist, _name)
+#define lws_metrics_priv_to_pub(_x) ((lws_metric_pub_t *)NULL)
+#endif
+
+#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API)
+/*
+ * sspc-specific version that also appends the tag value to the lifecycle tag
+ * used for logging the sspc identity
+ */
+int
+lws_metrics_tag_sspc_add(struct lws_sspc_handle *ss, const char *name, const char *val);
+#endif
+
+int
+lws_metrics_register_policy(struct lws_context *ctx,
+			    const lws_metric_policy_t *head);
+
+void
+lws_metrics_destroy(struct lws_context *ctx);
+
+void
+lws_metric_event(lws_metric_t *mt, char go_nogo, u_mt_t val);
+
+lws_metric_t *
+lws_metric_create(struct lws_context *ctx, uint8_t flags, const char *name);
+
+int
+lws_metric_destroy(lws_metric_t **mt, int keep);
+
+void
+lws_metric_policy_dyn_destroy(lws_metric_policy_dyn_t *dm, int keep);
+
+void
+lws_metric_rebind_policies(struct lws_context *ctx);
diff --git a/lib/system/smd/smd.c b/lib/system/smd/smd.c
index 6ba05f986ad66854f385a13dbdf5b256aa278e21..7630701690f120a5b1a7a146d9f5511a139a4b96 100644
--- a/lib/system/smd/smd.c
+++ b/lib/system/smd/smd.c
@@ -377,6 +377,10 @@ _lws_smd_ss_rx_forward(struct lws_context *ctx, const char *tag,
 
 	_class = (lws_smd_class_t)lws_ser_ru64be(buf);
 
+	if (_class == LWSSMDCL_METRICS) {
+
+	}
+
 	/* only locally forward messages that we care about in this process */
 
 	if (!(ctx->smd._class_filter & _class))
diff --git a/lib/tls/mbedtls/mbedtls-client.c b/lib/tls/mbedtls/mbedtls-client.c
index 38f8a33b9024d0b07015c51f31760d0654cf8395..cd9a585b83071671746e5077c0fa45ad702cd917 100644
--- a/lib/tls/mbedtls/mbedtls-client.c
+++ b/lib/tls/mbedtls/mbedtls-client.c
@@ -223,47 +223,75 @@ int
 lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len)
 {
 	int n;
+	unsigned int avoid = 0;
 	X509 *peer = SSL_get_peer_certificate(wsi->tls.ssl);
 	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];
+	const char *type = "";
 	char *sb = (char *)&pt->serv_buf[0];
 
 	if (!peer) {
+#if defined(LWS_WITH_SYS_METRICS)
+		lws_metrics_hist_bump_describe_wsi(wsi, lws_metrics_priv_to_pub(
+					wsi->a.context->mth_conn_failures),
+						   "tls=\"nocert\"");
+#endif
 		lwsl_info("peer did not provide cert\n");
 		lws_snprintf(ebuf, ebuf_len, "no peer cert");
 
 		return -1;
 	}
-	lwsl_info("peer provided cert\n");
 
 	n = (int)SSL_get_verify_result(wsi->tls.ssl);
-        lwsl_debug("get_verify says %d\n", n);
+	lwsl_debug("get_verify says %d\n", n);
 
-	if (n == X509_V_OK)
+	switch (n) {
+	case X509_V_OK:
 		return 0;
 
-	if (n == X509_V_ERR_HOSTNAME_MISMATCH &&
-	    (wsi->tls.use_ssl & LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK)) {
-		lwsl_info("accepting certificate for invalid hostname\n");
-		return 0;
+	case X509_V_ERR_HOSTNAME_MISMATCH:
+		type = "hostname";
+		avoid = LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
+		break;
+
+	case X509_V_ERR_INVALID_CA:
+		type = "invalidca";
+		avoid = LCCSCF_ALLOW_SELFSIGNED;
+		break;
+
+	case X509_V_ERR_CERT_NOT_YET_VALID:
+		type = "notyetvalid";
+		avoid = LCCSCF_ALLOW_EXPIRED;
+		break;
+
+	case X509_V_ERR_CERT_HAS_EXPIRED:
+		type = "expired";
+		avoid = LCCSCF_ALLOW_EXPIRED;
+		break;
 	}
 
-	if (n == X509_V_ERR_INVALID_CA &&
-	    (wsi->tls.use_ssl & LCCSCF_ALLOW_SELFSIGNED)) {
-		lwsl_info("accepting certificate from untrusted CA\n");
-		return 0;
+	lwsl_info("%s: cert problem: %s\n", __func__, type);
+#if defined(LWS_WITH_SYS_METRICS)
+	{
+		char buckname[64];
+		lws_snprintf(buckname, sizeof(buckname), "tls=\"%s\"", type);
+		lws_metrics_hist_bump_describe_wsi(wsi,
+		     lws_metrics_priv_to_pub(wsi->a.context->mth_conn_failures),
+			      buckname);
 	}
-
-	if ((n == X509_V_ERR_CERT_NOT_YET_VALID ||
-	     n == X509_V_ERR_CERT_HAS_EXPIRED) &&
-	     (wsi->tls.use_ssl & LCCSCF_ALLOW_EXPIRED)) {
-		lwsl_info("accepting expired or not yet valid certificate\n");
+#endif
+	if (wsi->tls.use_ssl & avoid) {
+		lwsl_info("%s: allowing anyway\n", __func__);
 
 		return 0;
 	}
+
 	lws_snprintf(ebuf, ebuf_len,
-		"server's cert didn't look good, (use_ssl 0x%x) X509_V_ERR = %d: %s\n",
-		(unsigned int)wsi->tls.use_ssl, n, ERR_error_string((unsigned long)n, sb));
+		"server's cert didn't look good, %s (use_ssl 0x%x) X509_V_ERR = %d: %s\n",
+		type, (unsigned int)wsi->tls.use_ssl, n,
+		ERR_error_string((unsigned long)n, sb));
+
 	lwsl_info("%s\n", ebuf);
+
 	lws_tls_err_describe_clear();
 
 	return -1;
diff --git a/lib/tls/mbedtls/mbedtls-ssl.c b/lib/tls/mbedtls/mbedtls-ssl.c
index 16fe2261fe415bdc2b37b22c50933ed9e8cd0da5..2404806438679bb628ce4de766b6739074bf0756 100644
--- a/lib/tls/mbedtls/mbedtls-ssl.c
+++ b/lib/tls/mbedtls/mbedtls-ssl.c
@@ -51,8 +51,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 	if (!wsi->tls.ssl)
 		return lws_ssl_capable_read_no_ssl(wsi, buf, len);
 
-	lws_stats_bump(pt, LWSSTATS_C_API_READ, 1);
-
 	errno = 0;
 	n = SSL_read(wsi->tls.ssl, buf, (int)len);
 #if defined(LWS_PLAT_FREERTOS)
@@ -61,15 +59,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 		return LWS_SSL_CAPABLE_ERROR;
 	}
 #endif
-#if defined(LWS_WITH_STATS)
-	if (!wsi->seen_rx && wsi->accept_start_us) {
-                lws_stats_bump(pt, LWSSTATS_US_SSL_RX_DELAY_AVG,
-			lws_now_usecs() - wsi->accept_start_us);
-                lws_stats_bump(pt, LWSSTATS_C_SSL_CONNS_HAD_RX, 1);
-		wsi->seen_rx = 1;
-	}
-#endif
-
 
 	lwsl_debug("%s: %s: SSL_read says %d\n", __func__, lws_wsi_tag(wsi), n);
 	/* manpage: returning 0 means connection shut down */
@@ -82,14 +71,13 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 	if (n < 0) {
 		m = SSL_get_error(wsi->tls.ssl, n);
 		lwsl_debug("%s: %s: ssl err %d errno %d\n", __func__, lws_wsi_tag(wsi), m, errno);
-		if (errno == LWS_ENOTCONN) {
+		if (errno == LWS_ENOTCONN)
 			/* If the socket isn't connected anymore, bail out. */
-			wsi->socket_is_permanently_unusable = 1;
-			return LWS_SSL_CAPABLE_ERROR;
-		}
+			goto do_err1;
+
 		if (m == SSL_ERROR_ZERO_RETURN ||
 		    m == SSL_ERROR_SYSCALL)
-			return LWS_SSL_CAPABLE_ERROR;
+			goto do_err;
 
 		if (m == SSL_ERROR_WANT_READ || SSL_want_read(wsi->tls.ssl)) {
 			lwsl_debug("%s: WANT_READ\n", __func__);
@@ -101,8 +89,16 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 			lwsl_debug("%s: LWS_SSL_CAPABLE_MORE_SERVICE\n", lws_wsi_tag(wsi));
 			return LWS_SSL_CAPABLE_MORE_SERVICE;
 		}
+
+do_err1:
 		wsi->socket_is_permanently_unusable = 1;
 
+do_err:
+#if defined(LWS_WITH_SYS_METRICS)
+	if (wsi->a.vhost)
+		lws_metric_event(wsi->a.vhost->mt_traffic_rx, METRES_NOGO, 0);
+#endif
+
 		return LWS_SSL_CAPABLE_ERROR;
 	}
 
@@ -115,23 +111,12 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 	lwsl_hexdump_notice(buf, n);
 #endif
 
-	lws_stats_bump(pt, LWSSTATS_B_READ, (uint64_t)n);
-
-#if defined(LWS_WITH_SERVER_STATUS)
+#if defined(LWS_WITH_SYS_METRICS)
 	if (wsi->a.vhost)
-		wsi->a.vhost->conn_stats.rx = wsi->a.vhost->conn_stats.rx + (unsigned long long)n;
-#endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (context->detailed_latency_cb) {
-		wsi->detlat.req_size = len;
-		wsi->detlat.acc_size = n;
-		wsi->detlat.type = LDLT_READ;
-		wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] =
-			lws_now_usecs() - pt->ust_left_poll;
-		wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-		lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-	}
+		lws_metric_event(wsi->a.vhost->mt_traffic_rx,
+				 METRES_GO /* rx */, (u_mt_t)n);
 #endif
+
 	/*
 	 * if it was our buffer that limited what we read,
 	 * check if SSL has additional data pending inside SSL buffers.
@@ -186,8 +171,14 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len)
 		return lws_ssl_capable_write_no_ssl(wsi, buf, len);
 
 	n = SSL_write(wsi->tls.ssl, buf, (int)len);
-	if (n > 0)
+	if (n > 0) {
+#if defined(LWS_WITH_SYS_METRICS)
+		if (wsi->a.vhost)
+			lws_metric_event(wsi->a.vhost->mt_traffic_tx,
+					 METRES_GO, (u_mt_t)n);
+#endif
 		return n;
+	}
 
 	m = SSL_get_error(wsi->tls.ssl, n);
 	if (m != SSL_ERROR_SYSCALL) {
@@ -208,6 +199,12 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len)
 	lwsl_debug("%s failed: %d\n",__func__, m);
 	wsi->socket_is_permanently_unusable = 1;
 
+#if defined(LWS_WITH_SYS_METRICS)
+		if (wsi->a.vhost)
+			lws_metric_event(wsi->a.vhost->mt_traffic_tx,
+					 METRES_NOGO, (u_mt_t)n);
+#endif
+
 	return LWS_SSL_CAPABLE_ERROR;
 }
 
diff --git a/lib/tls/openssl/openssl-client.c b/lib/tls/openssl/openssl-client.c
index 1c6e08a0549e32fc8581d5c4d640bb9bb9019b20..590ed86b946eeb5b979512e815f77bb8971f8c0d 100644
--- a/lib/tls/openssl/openssl-client.c
+++ b/lib/tls/openssl/openssl-client.c
@@ -125,6 +125,18 @@ OpenSSL_client_verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx)
 			lwsl_err("SSL error: %s (preverify_ok=%d;err=%d;"
 				 "depth=%d)\n", msg, preverify_ok, err, depth);
 
+#if defined(LWS_WITH_SYS_METRICS)
+			{
+				char buckname[64];
+
+				lws_snprintf(buckname, sizeof(buckname),
+					     "tls=\"%s\"", msg);
+				lws_metrics_hist_bump_describe_wsi(wsi,
+					lws_metrics_priv_to_pub(wsi->a.context->mth_conn_failures),
+					buckname);
+			}
+#endif
+
 			return preverify_ok;	// not ok
 		}
 	}
@@ -480,7 +492,8 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len)
 #if !defined(USE_WOLFSSL)
 	struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi];
 	char *p = (char *)&pt->serv_buf[0];
-	const char *es;
+	const char *es, *type = "";
+	unsigned int avoid = 0;
 	char *sb = p;
 	long n;
 
@@ -488,29 +501,46 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len)
 	ERR_clear_error();
 	n = SSL_get_verify_result(wsi->tls.ssl);
 
-	lwsl_debug("get_verify says %ld\n", n);
-
-	if (n == X509_V_OK)
+	switch (n) {
+	case X509_V_OK:
 		return 0;
 
-	if ((n == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT ||
-	     n == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) &&
-	     (wsi->tls.use_ssl & LCCSCF_ALLOW_SELFSIGNED)) {
-		lwsl_info("accepting self-signed certificate\n");
-
-		return 0;
-	}
-	if ((n == X509_V_ERR_CERT_NOT_YET_VALID ||
-	     n == X509_V_ERR_CERT_HAS_EXPIRED) &&
-	     (wsi->tls.use_ssl & LCCSCF_ALLOW_EXPIRED)) {
-		lwsl_info("accepting expired certificate\n");
-		return 0;
+	case X509_V_ERR_HOSTNAME_MISMATCH:
+		type = "tls=hostname";
+		avoid = LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
+		break;
+
+	case X509_V_ERR_INVALID_CA:
+	case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
+	case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
+		type = "tls=invalidca";
+		avoid = LCCSCF_ALLOW_SELFSIGNED;
+		break;
+
+	case X509_V_ERR_CERT_NOT_YET_VALID:
+		type = "tls=notyetvalid";
+		avoid = LCCSCF_ALLOW_EXPIRED;
+		break;
+
+	case X509_V_ERR_CERT_HAS_EXPIRED:
+		type = "tls=expired";
+		avoid = LCCSCF_ALLOW_EXPIRED;
+		break;
 	}
-	if (n == X509_V_ERR_CERT_NOT_YET_VALID) {
-		lwsl_info("Cert is from the future... "
-			    "probably our clock... accepting...\n");
+
+	lwsl_info("%s: cert problem: %s\n", __func__, type);
+
+#if defined(LWS_WITH_SYS_METRICS)
+	lws_metrics_hist_bump_describe_wsi(wsi,
+			lws_metrics_priv_to_pub(wsi->a.context->mth_conn_failures), type);
+#endif
+
+	if (wsi->tls.use_ssl & avoid) {
+		lwsl_info("%s: allowing anyway\n", __func__);
+
 		return 0;
 	}
+
 	es = ERR_error_string(
 	#if defined(LWS_WITH_BORINGSSL)
 					 (uint32_t)
@@ -519,8 +549,8 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len)
 	#endif
 					 n, sb);
 	lws_snprintf(ebuf, ebuf_len,
-		"server's cert didn't look good, X509_V_ERR = %ld: %s\n",
-		 n, es);
+		"server's cert didn't look good, %s X509_V_ERR = %ld: %s\n",
+		 type, n, es);
 	lwsl_info("%s\n", ebuf);
 	lws_tls_err_describe_clear();
 
diff --git a/lib/tls/openssl/openssl-ssl.c b/lib/tls/openssl/openssl-ssl.c
index a593d87685630548b93e3aa1c5e94ab255af9d6e..562c352dce3051b24b52cbf25e618c496afcf058 100644
--- a/lib/tls/openssl/openssl-ssl.c
+++ b/lib/tls/openssl/openssl-ssl.c
@@ -207,8 +207,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 	if (!wsi->tls.ssl)
 		return lws_ssl_capable_read_no_ssl(wsi, buf, len);
 
-	lws_stats_bump(pt, LWSSTATS_C_API_READ, 1);
-
 	errno = 0;
 	ERR_clear_error();
 	n = SSL_read(wsi->tls.ssl, buf, (int)(ssize_t)len);
@@ -218,16 +216,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 		return LWS_SSL_CAPABLE_ERROR;
 	}
 #endif
-#if defined(LWS_WITH_STATS)
-	if (!wsi->seen_rx && wsi->accept_start_us) {
-                lws_stats_bump(pt, LWSSTATS_US_SSL_RX_DELAY_AVG,
-                		      lws_now_usecs() -
-                			      wsi->accept_start_us);
-                lws_stats_bump(pt, LWSSTATS_C_SSL_CONNS_HAD_RX, 1);
-		wsi->seen_rx = 1;
-	}
-#endif
-
 
 	lwsl_debug("%s: SSL_read says %d\n", lws_wsi_tag(wsi), n);
 	/* manpage: returning 0 means connection shut down
@@ -258,7 +246,7 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 		m = lws_ssl_get_error(wsi, n);
 		lwsl_debug("%s: ssl err %d errno %d\n", lws_wsi_tag(wsi), m, errno);
 		if (m == SSL_ERROR_ZERO_RETURN) /* cleanly shut down */
-			return LWS_SSL_CAPABLE_ERROR;
+			goto do_err;
 
 		/* hm not retryable.. could be 0 size pkt or error  */
 
@@ -268,7 +256,12 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 			/* unclean, eg closed conn */
 
 			wsi->socket_is_permanently_unusable = 1;
-
+do_err:
+#if defined(LWS_WITH_SYS_METRICS)
+		if (wsi->a.vhost)
+			lws_metric_event(wsi->a.vhost->mt_traffic_rx,
+					 METRES_NOGO, 0);
+#endif
 			return LWS_SSL_CAPABLE_ERROR;
 		}
 
@@ -294,26 +287,12 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len)
 	 * paths to dump what was received as decrypted data from the tls tunnel
 	 */
 	lwsl_notice("%s: len %d\n", __func__, n);
-	lwsl_hexdump_notice(buf, n);
+	lwsl_hexdump_notice(buf, (unsigned int)n);
 #endif
 
-	lws_stats_bump(pt, LWSSTATS_B_READ, (unsigned int)n);
-
-#if defined(LWS_WITH_SERVER_STATUS)
+#if defined(LWS_WITH_SYS_METRICS)
 	if (wsi->a.vhost)
-		wsi->a.vhost->conn_stats.rx = (unsigned long long)(wsi->a.vhost->conn_stats.rx + (unsigned long long)(long long)n);
-#endif
-
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	if (context->detailed_latency_cb) {
-		wsi->detlat.req_size = len;
-		wsi->detlat.acc_size = (unsigned int)n;
-		wsi->detlat.type = LDLT_READ;
-		wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] =
-			(uint32_t)(lws_now_usecs() - pt->ust_left_poll);
-		wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-		lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-	}
+		lws_metric_event(wsi->a.vhost->mt_traffic_rx, METRES_GO, (u_mt_t)n);
 #endif
 
 	/*
@@ -363,7 +342,7 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len)
 	 * paths before sending data into the tls tunnel, where you can dump it
 	 * and see what is being sent.
 	 */
-	lwsl_notice("%s: len %d\n", __func__, len);
+	lwsl_notice("%s: len %u\n", __func__, (unsigned int)len);
 	lwsl_hexdump_notice(buf, len);
 #endif
 
@@ -373,8 +352,14 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len)
 	errno = 0;
 	ERR_clear_error();
 	n = SSL_write(wsi->tls.ssl, buf, (int)(ssize_t)len);
-	if (n > 0)
+	if (n > 0) {
+#if defined(LWS_WITH_SYS_METRICS)
+		if (wsi->a.vhost)
+			lws_metric_event(wsi->a.vhost->mt_traffic_tx,
+					 METRES_GO, (u_mt_t)n);
+#endif
 		return n;
+	}
 
 	m = lws_ssl_get_error(wsi, n);
 	if (m != SSL_ERROR_SYSCALL) {
@@ -398,6 +383,12 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len)
 
 	wsi->socket_is_permanently_unusable = 1;
 
+#if defined(LWS_WITH_SYS_METRICS)
+		if (wsi->a.vhost)
+			lws_metric_event(wsi->a.vhost->mt_traffic_tx,
+					 METRES_NOGO, 0);
+#endif
+
 	return LWS_SSL_CAPABLE_ERROR;
 }
 
diff --git a/lib/tls/tls-client.c b/lib/tls/tls-client.c
index b8f7a5112b1a43fab81c21df14698a025a32919e..e576f8cefe471bc27995a96e3919dfea447a2c40 100644
--- a/lib/tls/tls-client.c
+++ b/lib/tls/tls-client.c
@@ -34,6 +34,7 @@ lws_ssl_client_connect1(struct lws *wsi, char *errbuf, size_t len)
 	case LWS_SSL_CAPABLE_ERROR:
 		return -1;
 	case LWS_SSL_CAPABLE_DONE:
+		lws_metrics_caliper_report(wsi->cal_conn, METRES_GO);
 		return 1; /* connected */
 	case LWS_SSL_CAPABLE_MORE_SERVICE_WRITE:
 		lws_callback_on_writable(wsi);
@@ -73,8 +74,12 @@ lws_ssl_client_connect2(struct lws *wsi, char *errbuf, size_t len)
 		}
 	}
 
-	if (lws_tls_client_confirm_peer_cert(wsi, errbuf, len))
+	if (lws_tls_client_confirm_peer_cert(wsi, errbuf, len)) {
+		lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO);
 		return -1;
+	}
+
+	lws_metrics_caliper_report(wsi->cal_conn, METRES_GO);
 
 	return 1;
 }
@@ -187,6 +192,9 @@ lws_client_create_tls(struct lws *wsi, const char **pcce, int do_c1)
 		if (!do_c1)
 			return 0;
 
+		lws_metrics_caliper_report(wsi->cal_conn, METRES_GO);
+		lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mt_conn_tls);
+
 		n = lws_ssl_client_connect1(wsi, (char *)wsi->a.context->pt[(int)wsi->tsi].serv_buf,
 					    wsi->a.context->pt_serv_buf_size);
 		lwsl_debug("%s: lws_ssl_client_connect1: %d\n", __func__, n);
@@ -194,8 +202,10 @@ lws_client_create_tls(struct lws *wsi, const char **pcce, int do_c1)
 			return CCTLS_RETURN_RETRY; /* caller should return 0 */
 		if (n < 0) {
 			*pcce = (const char *)wsi->a.context->pt[(int)wsi->tsi].serv_buf;
+			lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO);
 			return CCTLS_RETURN_ERROR;
 		}
+		/* ...connect1 already handled caliper if SSL_accept done */
 	} else
 		wsi->tls.ssl = NULL;
 
diff --git a/lib/tls/tls-network.c b/lib/tls/tls-network.c
index 5deab5b6cf1db8f127e46f2ab5e4c3e93ae5208b..c90faa78e8ffd6b3e0a9e9c9e28be0edb0d59575 100644
--- a/lib/tls/tls-network.c
+++ b/lib/tls/tls-network.c
@@ -204,10 +204,6 @@ lws_gate_accepts(struct lws_context *context, int on)
 
 	lwsl_notice("%s: on = %d\n", __func__, on);
 
-#if defined(LWS_WITH_STATS)
-	context->updated = 1;
-#endif
-
 	while (v) {
 		if (v->tls.use_ssl && v->lserv_wsi &&
 		    lws_change_pollfd(v->lserv_wsi, (LWS_POLLIN) * !on,
diff --git a/lib/tls/tls-server.c b/lib/tls/tls-server.c
index c4983dfd1301ae57be3415a1b87fac38ecf73a64..887159fa2eae8814bc1b9241564c68f782dfbecc 100644
--- a/lib/tls/tls-server.c
+++ b/lib/tls/tls-server.c
@@ -157,9 +157,6 @@ lws_server_socket_service_ssl(struct lws *wsi, lws_sockfd_type accept_fd, char f
 			goto fail;
 		}
 
-#if defined(LWS_WITH_STATS)
-		context->updated = 1;
-#endif
 		/*
 		 * we are not accepted yet, but we need to enter ourselves
 		 * as a live connection.  That way we can retry when more
@@ -318,20 +315,13 @@ punt:
 
 		/* normal SSL connection processing path */
 
-#if defined(LWS_WITH_STATS)
-		/* only set this the first time around */
-		if (!wsi->accept_start_us)
-			wsi->accept_start_us = lws_now_usecs();
-#endif
 		errno = 0;
-		lws_stats_bump(pt, LWSSTATS_C_SSL_ACCEPT_SPIN, 1);
 		n = lws_tls_server_accept(wsi);
 		lwsl_info("SSL_accept says %d\n", n);
 		switch (n) {
 		case LWS_SSL_CAPABLE_DONE:
 			break;
 		case LWS_SSL_CAPABLE_ERROR:
-			lws_stats_bump(pt, LWSSTATS_C_SSL_CONNECTIONS_FAILED, 1);
 	                lwsl_info("%s: SSL_accept failed socket %u: %d\n",
 	                		__func__, wsi->desc.sockfd, n);
 			wsi->socket_is_permanently_unusable = 1;
@@ -341,26 +331,6 @@ punt:
 			return 0;
 		}
 
-		lws_stats_bump(pt, LWSSTATS_C_SSL_CONNECTIONS_ACCEPTED, 1);
-#if defined(LWS_WITH_STATS)
-		if (wsi->accept_start_us)
-			lws_stats_bump(pt,
-				      LWSSTATS_US_SSL_ACCEPT_LATENCY_AVG,
-				      lws_now_usecs() -
-					      wsi->accept_start_us);
-		wsi->accept_start_us = lws_now_usecs();
-#endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-		if (context->detailed_latency_cb) {
-			wsi->detlat.type = LDLT_TLS_NEG_SERVER;
-			wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] =
-				(uint32_t)(lws_now_usecs() -
-				wsi->detlat.earliest_write_req_pre_write);
-			wsi->detlat.latencies[LAT_DUR_USERCB] = 0;
-			lws_det_lat_cb(wsi->a.context, &wsi->detlat);
-		}
-#endif
-
 		/* adapt our vhost to match the SNI SSL_CTX that was chosen */
 		vh = context->vhost_list;
 		while (vh) {
diff --git a/lwsws/main.c b/lwsws/main.c
index d2b70b71c51840219574ff0830ef9fe4bb2e7844..eb19a0421b412a8eb5a640f72d77ce6f081fb521 100644
--- a/lwsws/main.c
+++ b/lwsws/main.c
@@ -64,7 +64,7 @@ static uv_signal_t signal_outer[2];
 static int pids[32];
 void lwsl_emit_stderr(int level, const char *line);
 
-#define LWSWS_CONFIG_STRING_SIZE (32 * 1024)
+#define LWSWS_CONFIG_STRING_SIZE (64 * 1024)
 
 static const struct lws_extension exts[] = {
 #if !defined(LWS_WITHOUT_EXTENSIONS)
diff --git a/minimal-examples/api-tests/api-test-lws_tokenize/main.c b/minimal-examples/api-tests/api-test-lws_tokenize/main.c
index b556789589b306aca789c9d948f1616f4f93ce4e..d69dc2fee17686ffe5c77e38c5d56858cf617278 100644
--- a/minimal-examples/api-tests/api-test-lws_tokenize/main.c
+++ b/minimal-examples/api-tests/api-test-lws_tokenize/main.c
@@ -204,7 +204,7 @@ struct tests tests[] = {
 	}, {
 		"fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5",
 		expected7, LWS_ARRAY_SIZE(expected7),
-		LWS_TOKENIZE_F_RFC7230_DELIMS
+		LWS_TOKENIZE_F_ASTERISK_NONTERM | LWS_TOKENIZE_F_RFC7230_DELIMS
 	},
 	{
 		" Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, greek",
@@ -720,6 +720,10 @@ int main(int argc, const char **argv)
 		lwsl_user("%s: wc 15 fail\n", __func__);
 		fail++;
 	}
+	if (lws_strcmp_wildcard("ssproxy.n.cn.*", 14, "ssproxy.n.cn.failures")) {
+		lwsl_user("%s: wc 16 fail\n", __func__);
+		fail++;
+	}
 
 	lwsl_user("Completed: PASS: %d, FAIL: %d\n", ok, fail);
 
diff --git a/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c b/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c
index 626e280499a86e6de36163159dfd6e3d15a9a075..8a447f2f4b8fd00e7704ce79b8d11fc3841a4b54 100644
--- a/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c
+++ b/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c
@@ -217,6 +217,30 @@ static const struct lws_protocols protocols[] = {
 	{ NULL, NULL, 0, 0 }
 };
 
+#if defined(LWS_WITH_SYS_METRICS)
+
+static int
+my_metric_report(lws_metric_pub_t *mp)
+{
+	lws_metric_bucket_t *sub = mp->u.hist.head;
+	char buf[192];
+
+	do {
+		if (lws_metrics_format(mp, &sub, buf, sizeof(buf)))
+			lwsl_user("%s: %s\n", __func__, buf);
+	} while ((mp->flags & LWSMTFL_REPORT_HIST) && sub);
+
+	/* 0 = leave metric to accumulate, 1 = reset the metric */
+
+	return 1;
+}
+
+static const lws_system_ops_t system_ops = {
+	.metric_report = my_metric_report,
+};
+
+#endif
+
 static void
 signal_cb(void *handle, int signum)
 {
@@ -362,6 +386,11 @@ int main(int argc, const char **argv)
 	 * network wsi) that we will use.
 	 */
 	info.fd_limit_per_thread = 1 + COUNT + 1;
+	info.pcontext = &context;
+
+#if defined(LWS_WITH_SYS_METRICS)
+	info.system_ops = &system_ops;
+#endif
 
 #if defined(LWS_WITH_MBEDTLS) || defined(USE_WOLFSSL)
 	/*
@@ -374,11 +403,6 @@ int main(int argc, const char **argv)
 	if ((p = lws_cmdline_option(argc, argv, "--limit")))
 		info.simultaneous_ssl_restriction = atoi(p);
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-results";
-#endif
-
 	context = lws_create_context(&info);
 	if (!context) {
 		lwsl_err("lws init failed\n");
diff --git a/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c b/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c
index dbc4e7248e7fcf9f4c01d0f9137acdb0ab705778..0049980c0db4479ed2fb90b357aa89054ae359b9 100644
--- a/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c
+++ b/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c
@@ -23,8 +23,41 @@
 
 static int interrupted;
 
-static const struct lws_http_mount mount = {
+#if defined(LWS_WITH_PLUGINS)
+static const char * const plugin_dirs[] = {
+	LWS_INSTALL_DATADIR"/libwebsockets-test-server/plugins/",
+	NULL
+};
+#endif
+
+static const struct lws_http_mount
+#if defined(LWS_WITH_SYS_METRICS)
+	mount_metrics = {
 	/* .mount_next */		NULL,		/* linked-list "next" */
+	/* .mountpoint */		"/metrics",		/* mountpoint URL */
+	/* .origin */			"lws-openmetrics", /* serve from dir */
+	/* .def */			"x",	/* default filename */
+	/* .protocol */			"lws-openmetrics",
+	/* .cgienv */			NULL,
+	/* .extra_mimetypes */		NULL,
+	/* .interpret */		NULL,
+	/* .cgi_timeout */		0,
+	/* .cache_max_age */		0,
+	/* .auth_mask */		0,
+	/* .cache_reusable */		0,
+	/* .cache_revalidate */		0,
+	/* .cache_intermediaries */	0,
+	/* .origin_protocol */		LWSMPRO_CALLBACK, /* bind to callback */
+	/* .mountpoint_len */		8,		/* char count */
+	/* .basic_auth_login_file */	NULL,
+	},
+#endif
+	mount = {
+#if defined(LWS_WITH_SYS_METRICS)
+	/* .mount_next */		&mount_metrics,		/* linked-list "next" */
+#else
+	/* .mount_next */		NULL,		/* linked-list "next" */
+#endif
 	/* .mountpoint */		"/",		/* mountpoint URL */
 	/* .origin */			"./mount-origin", /* serve from dir */
 	/* .def */			"index.html",	/* default filename */
@@ -94,6 +127,10 @@ int main(int argc, const char **argv)
 	info.ssl_cert_filepath = "localhost-100y.cert";
 	info.ssl_private_key_filepath = "localhost-100y.key";
 
+#if defined(LWS_WITH_PLUGINS)
+	info.plugin_dirs = plugin_dirs;
+#endif
+
 	if (lws_cmdline_option(argc, argv, "-h"))
 		info.options |= LWS_SERVER_OPTION_VHOST_UPG_STRICT_HOST_CHECK;
 
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c b/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c
index d07c4c723abfe0d64b5bba6880553ef0d2aebdfb..f6a24d7bf40bcc0faf5a777e592323898bca1d54 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c
@@ -389,11 +389,6 @@ int main(int argc, const char **argv)
 	info.port = CONTEXT_PORT_NO_LISTEN;
 	info.pprotocols = protocols;
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy";
-#endif
-
 	/* integrate us with lws system state management when context created */
 	nl.name = "app";
 	nl.notify_cb = app_system_state_nf;
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c b/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c
index bb9b8947b35f1e00e78f242116f1ad72a7b22d48..0f62d43bf73152fecc3dbd1adee5650fdf90b8f3 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c
@@ -100,11 +100,6 @@ int main(int argc, const char **argv)
 	info.protocols = lws_sspc_protocols;
 	info.port = CONTEXT_PORT_NO_LISTEN;
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy";
-#endif
-
 	/* integrate us with lws system state management when context created */
 	nl.name = "app";
 	nl.notify_cb = app_system_state_nf;
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c b/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c
index b3b18f6f599e5ba192a05447c0c1215f01577e7e..56ca531fafe7506446ad5f18337dd61a85a017b9 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c
@@ -341,11 +341,6 @@ int main(int argc, const char **argv)
 	}
 #endif
 
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy";
-#endif
-
 	/* integrate us with lws system state management when context created */
 	nl.name = "app";
 	nl.notify_cb = app_system_state_nf;
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-metrics-proxy/metrics-proxy-policy.json b/minimal-examples/secure-streams/minimal-secure-streams-metrics-proxy/metrics-proxy-policy.json
new file mode 100644
index 0000000000000000000000000000000000000000..078b025c6481e5617caf22fa6555d1124d89fd52
--- /dev/null
+++ b/minimal-examples/secure-streams/minimal-secure-streams-metrics-proxy/metrics-proxy-policy.json
@@ -0,0 +1,59 @@
+{
+	"release":"01234567",
+	"product":"myproduct",
+	"schema-version":1,
+	"retry": [{
+		"default": {
+			"backoff": [1000,2000,3000,5000,10000],
+			"conceal":5,
+			"jitterpc":20,
+			"svalidping":300,
+			"svalidhup":310
+		}}],
+	"certs": [{
+		"dst_root_x3": "MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ"},{"self_localhost": "MIIF5jCCA86gAwIBAgIJANq50IuwPFKgMA0GCSqGSIb3DQEBCwUAMIGGMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRXJld2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEbMBkGA1UECgwSbGlid2Vic29ja2V0cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEG5vbmVAaW52YWxpZC5vcmcwIBcNMTgwMzIwMDQxNjA3WhgPMjExODAyMjQwNDE2MDdaMIGGMQswCQYDVQQGEwJHQjEQMA4GA1UECAwHRXJld2hvbjETMBEGA1UEBwwKQWxsIGFyb3VuZDEbMBkGA1UECgwSbGlid2Vic29ja2V0cy10ZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEG5vbmVAaW52YWxpZC5vcmcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCjYtuWaICCY0tJPubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8Di3DAmHKnSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTekLWcfI5ZZtoGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnHKT/m6DSU0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6jzhNyMBTJ1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQUjy5N8pSNp7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAzTK4l2pHNuC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBKIzv9cgi9fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0nPN1IMSnzXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzoGMTvP/AuehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9psNcjTMaBQLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABo1MwUTAdBgNVHQ4EFgQU9mYU23tW2zsomkKTAXarjr2vjuswHwYDVR0jBBgwFoAU9mYU23tW2zsomkKTAXarjr2vjuswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEANjIBMrowYNCbhAJdP7dhlhT2RUFRdeRUJD0IxrH/hkvb6myHHnK8nOYezFPjUlmRKUgNEDuAxbnXZzPdCRNV9V2mShbXvCyiDY7WCQE2Bn44z26O0uWVk+7DNNLH9BnkwUtOnM9PwtmD9phWexm4q2GnTsiL6Ul6cy0QlTJWKVLEUQQ6yda582e23J1AXqtqFcpfoE34H3afEiGy882b+ZBiwkeV+oq6XVF8sFyr9zYrv9CvWTYlkpTQfLTZSsgPdEHYVcjvxQ2D+XyDR0aRLRlvxUa9dHGFHLICG34Juq5Ai6lM1EsoD8HSsJpMcmrH7MWw2cKkujC3rMdFTtte83wF1uuF4FjUC72+SmcQN7A386BC/nk2TTsJawTDzqwOu/VdZv2g1WpTHlumlClZeP+G/jkSyDwqNnTu1aodDmUa4xZodfhP1HWPwUKFcq8oQr148QYAAOlbUOJQU7QwRWd1VbnwhDtQWXC92A2w1n/xkZSR1BM/NUSDhkBSUU1WjMbWg6GgmnIZLRerQCu1Oozr87rOQqQakPkyt8BUSNK3K42j2qcfhAONdRl8Hq8Qs5pupy+s8sdCGDlwR3JNCMv6u48OK87F4mcIxhkSefFJUFII25pCGN5WtE4p5l+9cnO1GrIXe2Hl/7M0c/lbZ4FvXgARlex2rkgS0Ka06HE="},{"self_localhost_key": "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCjYtuWaICCY0tJPubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8Di3DAmHKnSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTekLWcfI5ZZtoGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnHKT/m6DSU0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6jzhNyMBTJ1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQUjy5N8pSNp7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAzTK4l2pHNuC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBKIzv9cgi9fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0nPN1IMSnzXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzoGMTvP/AuehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9psNcjTMaBQLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABAoICAFWe8MQZb37k2gdAV3Y6aq8fqokKQqbCNLd3giGFwYkezHXoJfg6Di7oZxNcKyw35LFEghkgtQqErQqo35VPIoH+vXUpWOjnCmM4muFA9/cX6mYMc8TmJsg0ewLdBCOZVw+wPABlaqz+0UOiSMMftpk9fz9JwGd8ERyBsT+tk3Qi6D0vPZVsC1KqxxL/cwIFd3Hf2ZBtJXe0KBn1pktWht5AKqx9mld2Ovl7NjgiC1Fx9r+fZw/iOabFFwQA4dr+R8mEMK/7bd4VXfQ1o/QGGbMTG+ulFrsiDyP+rBIAaGC0i7gDjLAIBQeDhP409ZhswIEc/GBtODU372a2CQK/u4Q/HBQvuBtKFNkGUooLgCCbFxzgNUGc83GB/6IwbEM7R5uXqsFiE71LpmroDyjKTlQ8YZkpIcLNVLw0usoGYHFm2rvCyEVlfsE3Ub8cFyTFk50SeOcF2QL2xzKmmbZEpXglxBHR0hjgon0IKJDGfor4bHO7Nt+1Ece8u2oTEKvpz5aIn44OeC5mApRGy83/0bvsesnWjDE/bGpoT8qFuy+0urDEPNId44XcJm1IRIlG56ErxC3l0s11wrIpTmXXckqwzFR9s2z7f0zjeyxqZg4NTPI7wkM3M8BXlvp2GTBIeoxrWB4V3YArwu8QF80QBgVzmgHl24nTg00UH1OjZsABAoIBAQDOxftSDbSqGytcWqPYP3SZHAWDA0O4ACEM+eCwau9ASutl0IDlNDMJ8nC2ph25BMe5hHDWp2cGQJog7pZ/3qQogQho2gUniKDifN7740QdykllTzTVROqmP8+efreIvqlzHmuqaGfGs5oTkZaWj5su+B+bT+9rIwZcwfs5YRINhQRx17qa++xh5mfE25c+M9fiIBTiNSo4lTxWMBShnK8xrGaMEmN7W0qTMbFHPgQz5FcxRjCCqwHilwNBeLDTp/ZECEB7y34khVh531mBE2mNzSVIQcGZP1I/DvXjW7UUNdgFwii/GW+6M0uUDy23UVQpbFzcV8o1C2nZc4Fb4zwBAoIBAQDKSJkFwwuRnaVJS6WxOKjX8MCu9/cKPnwBv2mmI2jgGxHTw5sr3ahmF5eTb8Zo19BowytN+tr62ZFoIBA9Ubc9esEAU8l3fggdfM82cuR9sGcfQVoCh8tMg6BP8IBLOmbSUhN3PG2m39I802u0fFNVQCJKhx1m1MFFLOu7lVcDS9JN+oYVPb6MDfBLm5jOiPuYkFZ4gH79J7gXI0/YKhaJ7yXthYVkdrSF6Eooer4RZgma62Dd1VNzSq3JBo6rYjF7Lvd+RwDCR1thHrmf/IXplxpNVkoMVxtzbrrbgnC25QmvRYc0rlS/kvM4yQhMH3eA7IycDZMpY+0xm7I7jTT7AoIBAGKzKIMDXdCxBWKhNYJ8z7hiItNl1IZZMW2TPUiY0rl6yaChBVXjM9W0r07QPnHZsUiByqb743adkbTUjmxdJzjaVtxN7ZXwZvOVrY7I7fPWYnCEfXCr4+IVpZI/ZHZWpGX6CGSgT6EOjCZ5IUufIvEpqVSmtF8MqfXO9o9uIYLokrWQx1dBl5UnuTLDqw8bChq7O5y6yfuWaOWvL7nxI8NvSsfj4y635gIa/0dFeBYZEfHIUlGdNVomwXwYEzgE/c19ruIowX7HU/NgxMWTMZhpazlxgesXybel+YNcfDQ4e3RMOMz3ZFiaMaJsGGNf4++d9TmMgk4Ns6oDs6Tb9AECggEBAJYzd+SOYo26iBu3nw3L65uEeh6xou8pXH0Tu4gQrPQTRZZ/nT3iNgOwqu1gRuxcq7TOjt41UdqIKO8vN7/AaJavCpaKoIMowy/aGCbvAvjNPpU3unU8jdl/t08EXs79S5IKPcgAx87sTTi7KDN5SYt4tr2uPEe53NTXuSatilG5QCyExIELOuzWAMKzg7CAiIlNS9foWeLyVkBgCQ6Sme/L8ta+mUDy37K6vC34jh9vK9yrwF6X44ItRoOJafCaVfGI+175q/eWcqTX4q+IG4tKls4sL4mgOJLq+ra50aYMxbcuommctPMXU6CrrYyQpPTHMNVDQy2ttFdsq9iKTncCggEBAMmt/8yvPflS+xv3kg/ZBvR9JB1In2n3rUCYYD47ReKFqJ03Vmq5C9nY56s9w7OUO8perBXlJYmKZQhO4293lvxZD2Iq4NcZbVSCMoHAUzhzY3brdgtSIxa2gGveGAezZ38qKIU26dkz7deECY4vrsRkwhpTW0LGVCpjcQoaKvymAoCmAs8V2oMrZiw1YQ9uOUoWwOqm1wZqmVcOXvPIS2gWAs3fQlWjH9hkcQTMsUaXQDOD0aqkSY3ENqOvbCV1/oUpRi3076khCoAXI1bKSn/AvR3KDP14B5toHI/F5OTSEiGhhHesgRrsfBrpEY1IATtPq1taBZZogRqI3rOkkPk="
+	}],
+	"trust_stores": [
+		{"name": "le_via_dst",
+		 "stack": ["dst_root_x3"]
+		}
+	], "s": [
+		{
+			"mintest": {
+				"endpoint":"warmcat.com",
+				"port":443,
+				"protocol":"h2",
+				"http_method":"GET",
+				"http_url":"index.html",
+				"tls":true,
+				"retry":"default",
+				"tls_trust_store":"le_via_dst"
+		}},{
+			"forscraper": {
+				"server":true,
+				"port":19090,
+				"protocol":"h1",
+				"metadata": [{
+					"mime": "Content-Type:",
+					"method": "",
+					"path": ""
+				}
+			]
+		}},{
+			"forclients": {
+				"server":true,
+				"port":19091,
+				"protocol":"h1",
+				"metadata": [{
+					"mime": "Content-Type:",
+					"method": "",
+					"path": ""
+				}],
+				"tls":true,
+				"ws_subprotocol":"lws-metrics-proxy",
+				"server_cert":"self_localhost",
+				"server_key":"self_localhost_key"
+		}}
+	]
+}
+
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c b/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c
index 8a649ad6d1c55d67ac7c67f180f80e53ce4d544a..a213135a1897284b5d468ff69f0462cf4b174ba2 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c
@@ -500,10 +500,6 @@ int main(int argc, const char **argv)
 	info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS |
 		       LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
 #endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy";
-#endif
 
 	/* integrate us with lws system state management when context created */
 
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c b/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c
index b8f428f70c6a614fbab447b4fcc3909d53718c59..122cf7c96f1d7bc765432419286d7b4e288df6eb 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c
@@ -206,6 +206,30 @@ static lws_state_notify_link_t * const app_notifier_list[] = {
 	&nl, NULL
 };
 
+#if defined(LWS_WITH_SYS_METRICS)
+
+static int
+my_metric_report(lws_metric_pub_t *mp)
+{
+	lws_metric_bucket_t *sub = mp->u.hist.head;
+	char buf[192];
+
+	do {
+		if (lws_metrics_format(mp, &sub, buf, sizeof(buf)))
+			lwsl_user("%s: %s\n", __func__, buf);
+	} while ((mp->flags & LWSMTFL_REPORT_HIST) && sub);
+
+	/* 0 = leave metric to accumulate, 1 = reset the metric */
+
+	return 1;
+}
+
+static const lws_system_ops_t system_ops = {
+	.metric_report = my_metric_report,
+};
+
+#endif
+
 static void
 sigint_handler(int sig)
 {
@@ -262,13 +286,9 @@ int main(int argc, const char **argv)
 	info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS |
 		       LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW |
 		       LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
-	info.fd_limit_per_thread = 1 + 32 + 1;
+	info.fd_limit_per_thread = 1 + 6 + 1;
 	info.pss_policies_json = default_ss_policy;
 	info.port = CONTEXT_PORT_NO_LISTEN;
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy";
-#endif
 
 	/* integrate us with lws system state management when context created */
 	nl.name = "app";
@@ -278,6 +298,11 @@ int main(int argc, const char **argv)
 	info.pt_serv_buf_size = (unsigned int)((6144 * 2) + 2048);
 	info.max_http_header_data = (unsigned short)(6144 + 2048);
 
+#if defined(LWS_WITH_SYS_METRICS)
+	info.system_ops = &system_ops;
+	info.metrics_prefix = "ssproxy";
+#endif
+
 	context = lws_create_context(&info);
 	if (!context) {
 		lwsl_err("lws init failed\n");
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c b/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c
index a0d028635c279443d53b421ed7e81b63c441b277..741e6932d2081f97ad186576458d0a0e957cef03 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c
@@ -178,6 +178,7 @@ static const lws_ss_info_t ssi_lws_smd = {
 	.user_alloc		  = sizeof(myss_t),
 	.streamtype		  = LWS_SMD_STREAMTYPENAME,
 	.manual_initial_tx_credit = LWSSMDCL_SYSTEM_STATE |
+				    LWSSMDCL_METRICS |
 				    LWSSMDCL_NETWORK,
 };
 
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c b/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c
index d276c21644c3d5de1ada18537ba06014c3e70570..1cffe36815ae070f7aaaf5906eed0163bb6a217d 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c
@@ -230,10 +230,6 @@ int main(int argc, const char **argv)
 	info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS |
 		       LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
 #endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy";
-#endif
 
 	/* integrate us with lws system state management when context created */
 
diff --git a/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c b/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c
index 95876847af28b8da593f89f73972a2fc48000ca2..e5339e14720a747e0c63afa8a0971a4c7fe6abcb 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c
@@ -350,6 +350,58 @@ static const char * const default_ss_policy =
 			"\"opportunistic\": true,"
 			"\"retry\": \"default\","
 			"\"tls_trust_store\": \"arca1\""
+
+		"}},{"
+
+		/*
+		 * Various kinds of tls failure
+		 *
+		 * hostname.badcert.warmcat.com: serves valid cert but for
+		 *				 warmcat.com
+		 *
+		 * warmcat.com:446: serves valid but expired cert
+		 *
+		 * I don't have an easy way to make the test for "not valid yet"
+		 * cert without root
+		 *
+		 * invalidca.badcert.warmcat.com:  selfsigned cert for that
+		 *				   hostname
+		 */
+
+		    "\"badcert_hostname\": {"
+			"\"endpoint\": \"hostname.badcert.warmcat.com\","
+			"\"port\": 443,"
+			"\"protocol\": \"h1\","
+			"\"http_method\": \"GET\","
+			"\"http_url\": \"/\","
+			"\"tls\": true,"
+			"\"opportunistic\": true,"
+			"\"retry\": \"default\","
+			"\"tls_trust_store\": \"le_via_dst\""
+		"}},{"
+		    "\"badcert_expired\": {"
+			"\"endpoint\": \"warmcat.com\","
+			"\"port\": 446,"
+			"\"protocol\": \"h1\","
+			"\"http_method\": \"GET\","
+			"\"http_url\": \"/\","
+			"\"tls\": true,"
+			"\"opportunistic\": true,"
+			"\"retry\": \"default\","
+			"\"tls_trust_store\": \"le_via_dst\""
+		"}},{"
+		    "\"badcert_selfsigned\": {"
+			"\"endpoint\": \"invalidca.badcert.warmcat.com\","
+			"\"port\": 443,"
+			"\"protocol\": \"h1\","
+			"\"http_method\": \"GET\","
+			"\"http_url\": \"/\","
+			"\"tls\": true,"
+			"\"nghttp2_quirk_end_stream\": true,"
+			"\"h2q_oflow_txcr\": true,"
+			"\"opportunistic\": true,"
+			"\"retry\": \"default\","
+			"\"tls_trust_store\": \"le_via_dst\""
                 "}}"
 	"]}"
 ;
@@ -495,6 +547,29 @@ struct tests_seq {
 		12345
 	},
 
+	/*
+	 * Let's fail at the tls negotiation various ways
+	 */
+
+	{
+		"h1:badcert_hostname",
+		"badcert_hostname", 5 * LWS_US_PER_SEC, LWSSSCS_TIMEOUT,
+		(1 << LWSSSCS_QOS_NACK_REMOTE) |
+		(1 << LWSSSCS_ALL_RETRIES_FAILED)
+	},
+	{
+		"h1:badcert_expired",
+		"badcert_expired", 5 * LWS_US_PER_SEC, LWSSSCS_TIMEOUT,
+		(1 << LWSSSCS_QOS_NACK_REMOTE) |
+		(1 << LWSSSCS_ALL_RETRIES_FAILED)
+	},
+	{
+		"h1:badcert_selfsigned",
+		"badcert_selfsigned", 5 * LWS_US_PER_SEC, LWSSSCS_TIMEOUT,
+		(1 << LWSSSCS_QOS_NACK_REMOTE) |
+		(1 << LWSSSCS_ALL_RETRIES_FAILED)
+	},
+
 };
 
 typedef struct myss {
@@ -675,6 +750,29 @@ static lws_state_notify_link_t * const app_notifier_list[] = {
 	&nl, NULL
 };
 
+#if defined(LWS_WITH_SYS_METRICS)
+static int
+my_metric_report(lws_metric_pub_t *mp)
+{
+	lws_metric_bucket_t *sub = mp->u.hist.head;
+	char buf[192];
+
+	do {
+		if (lws_metrics_format(mp, &sub, buf, sizeof(buf)))
+			lwsl_user("%s: %s\n", __func__, buf);
+	} while ((mp->flags & LWSMTFL_REPORT_HIST) && sub);
+
+	/* 0 = leave metric to accumulate, 1 = reset the metric */
+
+	return 1;
+}
+
+static const lws_system_ops_t system_ops = {
+	.metric_report = my_metric_report,
+};
+
+#endif
+
 static void
 sigint_handler(int sig)
 {
@@ -740,6 +838,13 @@ main(int argc, const char **argv)
 	nl.notify_cb = app_system_state_nf;
 	info.register_notifier_list = app_notifier_list;
 
+#if defined(LWS_WITH_SYS_METRICS)
+	info.system_ops = &system_ops;
+#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API)
+	info.metrics_prefix = "ssmex";
+#endif
+#endif
+
 	/* create the context */
 
 	context = lws_create_context(&info);
diff --git a/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c b/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c
index cd3f73adce059bdf74e01004c60384fd5ea09dd0..7000629425137736467e023f1b9b619f84ffd254 100644
--- a/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c
+++ b/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c
@@ -125,7 +125,7 @@ static const char * const default_ss_policy =
 #if defined(VIA_LOCALHOST_SOCKS)
 			"\"http_url\":"		"\"policy/minimal-proxy-socks.json\","
 #else
-			"\"http_url\":"		"\"policy/minimal-proxy-2.json\","
+			"\"http_url\":"		"\"policy/minimal-proxy-v4.2.json\","
 #endif
 			"\"tls\":"		"true,"
 			"\"opportunistic\":"	"true,"
@@ -363,6 +363,30 @@ static lws_state_notify_link_t * const app_notifier_list[] = {
 	&nl, NULL
 };
 
+#if defined(LWS_WITH_SYS_METRICS)
+
+static int
+my_metric_report(lws_metric_pub_t *mp)
+{
+	lws_metric_bucket_t *sub = mp->u.hist.head;
+	char buf[192];
+
+	do {
+		if (lws_metrics_format(mp, &sub, buf, sizeof(buf)))
+			lwsl_user("%s: %s\n", __func__, buf);
+	} while ((mp->flags & LWSMTFL_REPORT_HIST) && sub);
+
+	/* 0 = leave metric to accumulate, 1 = reset the metric */
+
+	return 1;
+}
+
+static const lws_system_ops_t system_ops = {
+	.metric_report = my_metric_report,
+};
+
+#endif
+
 static void
 sigint_handler(int sig)
 {
@@ -425,10 +449,6 @@ int main(int argc, const char **argv)
 		       LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW |
 		       LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
 #endif
-#if defined(LWS_WITH_DETAILED_LATENCY)
-	info.detailed_latency_cb = lws_det_lat_plot_cb;
-	info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy";
-#endif
 
 	/* integrate us with lws system state management when context created */
 
@@ -436,6 +456,12 @@ int main(int argc, const char **argv)
 	nl.notify_cb = app_system_state_nf;
 	info.register_notifier_list = app_notifier_list;
 
+
+#if defined(LWS_WITH_SYS_METRICS)
+	info.system_ops = &system_ops;
+	info.metrics_prefix = "ssmex";
+#endif
+
 	/* create the context */
 
 	context = lws_create_context(&info);
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index 2f252f9e2b7eb9724452797b08ad33b204ea6fc7..15acd5b96528b0539f828b25972f314612cc386c 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -147,13 +147,12 @@ if (LWS_ROLE_WS)
 			endif()
 		endif()
 
-	if (LWS_WITH_SERVER_STATUS)
-			create_plugin(protocol_lws_server_status ""
-				      "protocol_lws_server_status.c" "" "")
+	if (LWS_WITH_SYS_METRICS)
+			create_plugin(protocol_lws_openmetrics_export ""
+				      "protocol_lws_openmetrics_export.c" "" "")
 			if (NOT LWS_WITH_PLUGINS_BUILTIN)
-				target_compile_definitions(protocol_lws_server_status PRIVATE LWS_BUILDING_SHARED)
+				target_compile_definitions(protocol_lws_openmetrics_export PRIVATE LWS_BUILDING_SHARED)
 			endif()
-
 	endif()
 
 	if (NOT LWS_WITHOUT_CLIENT)
@@ -237,11 +236,6 @@ if (LWS_WITH_PLUGINS AND NOT LWS_WITH_PLUGINS_BUILTIN)
 	endif()
 
 
-if (LWS_WITH_SERVER_STATUS)
-	install(FILES server-status.html;server-status.js;server-status.css;lwsws-logo.png
-		DESTINATION share/libwebsockets-test-server/server-status
-			COMPONENT examples)
-endif()
 endif()
 
 export_to_parent_intermediate()
diff --git a/plugins/protocol_lws_openmetrics_export.c b/plugins/protocol_lws_openmetrics_export.c
new file mode 100644
index 0000000000000000000000000000000000000000..ba79fef33ed279f00922ba9e4db70f9f9fc136ba
--- /dev/null
+++ b/plugins/protocol_lws_openmetrics_export.c
@@ -0,0 +1,1200 @@
+/*
+ * libwebsockets-test-server - libwebsockets test implementation
+ *
+ * Written in 2010-2021 by Andy Green <andy@warmcat.com>
+ *
+ * This file is made available under the Creative Commons CC0 1.0
+ * Universal Public Domain Dedication.
+ *
+ * The person who associated a work with this deed has dedicated
+ * the work to the public domain by waiving all of his or her rights
+ * to the work worldwide under copyright law, including all related
+ * and neighboring rights, to the extent allowed by law. You can copy,
+ * modify, distribute and perform the work, even for commercial purposes,
+ * all without asking permission.
+ *
+ * The test apps are intended to be adapted for use in your code, which
+ * may be proprietary.  So unlike the library itself, they are licensed
+ * Public Domain.
+ *
+ * Scrapeable, proxiable OpenMetrics metrics (compatible with Prometheus)
+ *
+ * https://tools.ietf.org/html/draft-richih-opsawg-openmetrics-00
+ *
+ * This plugin provides four protocols related to openmetrics handling:
+ *
+ * 1) "lws-openmetrics" direct http listener so scraper can directly get metrics
+ *
+ * 2) "lws-openmetrics-prox-agg" metrics proxy server that scraper can connect
+ *    to locally to proxy through to connected remote clients at 3)
+ *
+ * 3) "lws-openmetrics-prox-server" metrics proxy server that remote clients can
+ *    connect to, providing a path where scrapers at 2) can get metrics from
+ *    clients connected us
+ *
+ * 4) "lws-openmetrics-prox-client" nailed-up metrics proxy client that tries to
+ *    keep up a connection to the server at 3), allowing to scraper to reach
+ *    clients that have no reachable way to serve.
+ *
+ * These are provided like this to maximize flexibility in being able to add
+ * openmetrics serving, proxying, or client->proxy to existing lws code.
+ *
+ * Openmetrics supports a "metric" at the top of its report that describes the
+ * source aka "target metadata".
+ *
+ * Since we want to enable collection from devices that are not externally
+ * reachable, we must provide a reachable server that the clients can attach to
+ * and have their stats aggregated and then read by Prometheus or whatever.
+ * Openmetrics says that it wants to present the aggregated stats in a flat
+ * summary with only the aggregator's "target metadata" and contributor targets
+ * getting their data tagged with the source
+ *
+ * "The above discussion is in the context of individual exposers.  An
+ *  exposition from a general purpose monitoring system may contain
+ *  metrics from many individual targets, and thus may expose multiple
+ *  target info Metrics.  The metrics may already have had target
+ *  metadata added to them as labels as part of ingestion.  The metric
+ *  names MUST NOT be varied based on target metadata.  For example it
+ *  would be incorrect for all metrics to end up being prefixed with
+ *  staging_ even if they all originated from targets in a staging
+ *  environment)."
+ */
+
+#if !defined (LWS_PLUGIN_STATIC)
+#if !defined(LWS_DLL)
+#define LWS_DLL
+#endif
+#if !defined(LWS_INTERNAL)
+#define LWS_INTERNAL
+#endif
+#include <libwebsockets.h>
+#endif
+#include <string.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#if !defined(WIN32)
+#include <unistd.h>
+#endif
+#include <assert.h>
+
+struct vhd {
+	struct lws_context	*cx;
+	struct lws_vhost	*vhost;
+
+	char			ws_server_uri[128];
+	char			metrics_proxy_path[128];
+	char			ba_secret[128];
+
+	const char		*proxy_side_bind_name;
+	/**< name used to bind the two halves of the proxy together, must be
+	 * the same name given in a pvo for both "lws-openmetrics-prox-agg"
+	 * (the side local to the scraper) and "lws-openmetrics-prox-server"
+	 * (the side the clients connect to)
+	 */
+
+	char			sanity[8];
+
+	lws_dll2_owner_t	clients;
+
+	lws_sorted_usec_list_t	sul;	     /* schedule connection retry */
+
+	struct vhd		*bind_partner_vhd;
+
+	struct lws		*wsi;	     /* related wsi if any */
+	uint16_t		retry_count; /* count of consequetive retries */
+};
+
+struct pss {
+	lws_dll2_t		list;
+	char			proxy_path[64];
+	struct lwsac		*ac;	/* the translated metrics, one ac per line */
+	struct lwsac		*walk;	/* iterator for ac when writing */
+	size_t			tot;	/* content-length computation */
+	struct lws		*wsi;
+
+	uint8_t			greet:1; /* set if client needs to send proxy path */
+	uint8_t			trigger:1; /* we want to ask the client to dump */
+};
+
+#if defined(LWS_WITH_CLIENT)
+static const uint32_t backoff_ms[] = { 1000, 2000, 3000, 4000, 5000 };
+
+static const lws_retry_bo_t retry = {
+	.retry_ms_table			= backoff_ms,
+	.retry_ms_table_count		= LWS_ARRAY_SIZE(backoff_ms),
+	.conceal_count			= LWS_ARRAY_SIZE(backoff_ms),
+
+	.secs_since_valid_ping		= 400,  /* force PINGs after secs idle */
+	.secs_since_valid_hangup	= 400, /* hangup after secs idle */
+
+	.jitter_percent			= 0,
+};
+
+static void
+omc_connect_client(lws_sorted_usec_list_t *sul)
+{
+	struct vhd *vhd = lws_container_of(sul, struct vhd, sul);
+	struct lws_client_connect_info i;
+	const char *prot;
+	char url[128];
+
+	memset(&i, 0, sizeof(i));
+
+	lwsl_notice("%s: %s %s %s\n", __func__, vhd->ws_server_uri, vhd->metrics_proxy_path, vhd->ba_secret);
+
+	lws_strncpy(url, vhd->ws_server_uri, sizeof(url));
+
+	if (lws_parse_uri(url, &prot, &i.address, &i.port, &i.path)) {
+		lwsl_err("%s: unable to parse uri %s\n", __func__,
+			 vhd->ws_server_uri);
+		return;
+	}
+
+	i.context		= vhd->cx;
+	i.origin		= i.address;
+	i.host			= i.address;
+	i.ssl_connection	= LCCSCF_USE_SSL;
+	i.protocol		= "lws-openmetrics-prox-server"; /* public subprot */
+	i.local_protocol_name	= "lws-openmetrics-prox-client";
+	i.pwsi			= &vhd->wsi;
+	i.retry_and_idle_policy = &retry;
+	i.userdata		= vhd;
+	i.vhost			= vhd->vhost;
+
+	lwsl_notice("%s: %s %u %s\n", __func__, i.address, i.port, i.path);
+
+	if (lws_client_connect_via_info(&i))
+		return;
+
+	/*
+	 * Failed... schedule a retry... we can't use the _retry_wsi()
+	 * convenience wrapper api here because no valid wsi at this
+	 * point.
+	 */
+	if (!lws_retry_sul_schedule(vhd->cx, 0, sul, &retry,
+				    omc_connect_client, &vhd->retry_count))
+		return;
+
+	vhd->retry_count = 0;
+	lws_retry_sul_schedule(vhd->cx, 0, sul, &retry,
+			       omc_connect_client, &vhd->retry_count);
+}
+#endif
+
+static void
+openmetrics_san(char *nm, size_t nl)
+{
+	size_t m;
+
+	/* Openmetrics has a very restricted token charset */
+
+	for (m = 0; m < nl; m++)
+		if ((nm[m] < 'A' || nm[m] > 'Z') &&
+		    (nm[m] < 'a' || nm[m] > 'z') &&
+		    (nm[m] < '0' || nm[m] > '9') &&
+		    nm[m] != '_')
+			nm[m] = '_';
+}
+
+static int
+lws_metrics_om_format_agg(lws_metric_pub_t *pub, const char *nm, lws_usec_t now,
+			  int gng, char *buf, size_t len)
+{
+	const char *_gng = gng ? "_nogo" : "_go";
+	char *end = buf + len - 1, *obuf = buf;
+
+	if (pub->flags & LWSMTFL_REPORT_ONLY_GO)
+		_gng = "";
+
+	if (!(pub->flags & LWSMTFL_REPORT_MEAN)) {
+		/* only the sum is meaningful */
+		if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US) {
+			buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
+				"%s_count %u\n"
+				"%s_us_sum %llu\n"
+				"%s_created %lu.%06u\n",
+				nm, (unsigned int)pub->u.agg.count[gng],
+				nm, (unsigned long long)pub->u.agg.sum[gng],
+				nm, (unsigned long)(pub->us_first / 1000000),
+				    (unsigned int)(pub->us_first % 1000000));
+
+			return lws_ptr_diff(buf, obuf);
+		}
+
+		/* it's a monotonic ordinal, like total tx */
+		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
+				    "%s%s_count %u\n"
+				    "%s%s_sum %llu\n",
+				    nm, _gng,
+				    (unsigned int)pub->u.agg.count[gng],
+				    nm, _gng,
+				    (unsigned long long)pub->u.agg.sum[gng]);
+
+	} else
+		buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf),
+				    "%s%s_count %u\n"
+				    "%s%s_mean %llu\n",
+				    nm, _gng,
+				    (unsigned int)pub->u.agg.count[gng],
+				    nm, _gng, (unsigned long long)
+				    (pub->u.agg.count[gng] ?
+						pub->u.agg.sum[gng] /
+						pub->u.agg.count[gng] : 0));
+
+	return lws_ptr_diff(buf, obuf);
+}
+
+static int
+lws_metrics_om_ac_stash(struct pss *pss, const char *buf, size_t len)
+{
+	char *q;
+
+	q = lwsac_use(&pss->ac, LWS_PRE + len + 2, LWS_PRE + len + 2);
+	if (!q) {
+		lwsac_free(&pss->ac);
+
+		return -1;
+	}
+	q[LWS_PRE] = (char)((len >> 8) & 0xff);
+	q[LWS_PRE + 1] = (char)(len & 0xff);
+	memcpy(q + LWS_PRE + 2, buf, len);
+	pss->tot += len;
+
+	return 0;
+}
+
+/*
+ * We have to do the ac listing at this level, because there can be too large
+ * a number to metrics tags to iterate that can fit in a reasonable buffer.
+ */
+
+static int
+lws_metrics_om_format(struct pss *pss, lws_metric_pub_t *pub, const char *nm)
+{
+	char buf[1200], *p = buf, *end = buf + sizeof(buf) - 1, tmp[512];
+	lws_usec_t t = lws_now_usecs();
+
+	if (pub->flags & LWSMTFL_REPORT_HIST) {
+		lws_metric_bucket_t *buck = pub->u.hist.head;
+
+		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+				  "%s_count %llu\n",
+				  nm, (unsigned long long)
+				  pub->u.hist.total_count);
+
+		while (buck) {
+			lws_strncpy(tmp, lws_metric_bucket_name(buck),
+				    sizeof(tmp));
+
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+					  "%s{%s} %llu\n", nm, tmp,
+					  (unsigned long long)buck->count);
+
+			lws_metrics_om_ac_stash(pss, buf,
+						lws_ptr_diff_size_t(p, buf));
+			p = buf;
+
+			buck = buck->next;
+		}
+
+		goto happy;
+	}
+
+	if (!pub->u.agg.count[METRES_GO] && !pub->u.agg.count[METRES_NOGO])
+		return 0;
+
+	if (pub->u.agg.count[METRES_GO])
+		p += lws_metrics_om_format_agg(pub, nm, t, METRES_GO, p,
+					       lws_ptr_diff_size_t(end, p));
+
+	if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO) &&
+	    pub->u.agg.count[METRES_NOGO])
+		p += lws_metrics_om_format_agg(pub, nm, t, METRES_NOGO, p,
+					       lws_ptr_diff_size_t(end, p));
+
+	if (pub->flags & LWSMTFL_REPORT_MEAN)
+		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+				  "%s_min %llu\n"
+				  "%s_max %llu\n",
+				  nm, (unsigned long long)pub->u.agg.min,
+				  nm, (unsigned long long)pub->u.agg.max);
+
+happy:
+	return lws_metrics_om_ac_stash(pss, buf, lws_ptr_diff_size_t(p, buf));
+}
+
+static int
+append_om_metric(lws_metric_pub_t *pub, void *user)
+{
+	struct pss *pss = (struct pss *)user;
+	char nm[64];
+	size_t nl;
+
+	/*
+	 * Convert lws_metrics to openmetrics metrics data, stashing into an
+	 * lwsac without backfill.  Since it's not backfilling, use areas are in
+	 * linear sequence simplifying walking them.  Limiting the lwsac alloc
+	 * to less than a typical mtu means we can write one per write
+	 * efficiently
+	 */
+
+	lws_strncpy(nm, pub->name, sizeof(nm));
+	nl = strlen(nm);
+
+	openmetrics_san(nm, nl);
+
+	return lws_metrics_om_format(pss, pub, nm);
+}
+
+#if defined(__linux__)
+static int
+grabfile(const char *fi, char *buf, size_t len)
+{
+	int n, fd = lws_open(fi, LWS_O_RDONLY);
+
+	buf[0] = '\0';
+	if (fd < 0)
+		return -1;
+
+	n = (int)read(fd, buf, len - 1);
+	close(fd);
+	if (n < 0) {
+		buf[0] = '\0';
+		return -1;
+	}
+
+	buf[n] = '\0';
+	if (n > 0 && buf[n - 1] == '\n')
+		buf[--n] = '\0';
+
+	return n;
+}
+#endif
+
+/*
+ * Let's pregenerate the output into an lwsac all at once and
+ * then spool it back to the peer afterwards
+ *
+ * - there's not going to be that much of it (a few kB)
+ * - we then know the content-length for the headers
+ * - it's stretchy to arbitrary numbers of metrics
+ * - lwsac block list provides the per-metric structure to
+ *   hold the data in a way we can walk to write it simply
+ */
+
+int
+ome_prepare(struct lws_context *ctx, struct pss *pss)
+{
+	char buf[1224], *start = buf + LWS_PRE, *p = start,
+	     *end = buf + sizeof(buf) - 1;
+	char hn[64];
+
+	pss->tot = 0;
+
+	/*
+	 * Target metadata
+	 */
+
+	hn[0] = '\0';
+	gethostname(hn, sizeof(hn) - 1);
+	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+			  "# TYPE target info\n"
+			  "# HELP target Target metadata\n"
+			  "target_info{hostname=\"%s\"", hn);
+
+#if defined(__linux__)
+	if (grabfile("/proc/self/cmdline", hn, sizeof(hn)))
+		p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p),
+				  ",cmdline=\"%s\"", hn);
+#endif
+
+	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "} 1\n");
+
+	if (lws_metrics_om_ac_stash(pss, (const char *)buf + LWS_PRE,
+				    lws_ptr_diff_size_t(p, buf + LWS_PRE)))
+		return 1;
+
+	/* lws version */
+
+	p = start;
+	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+			  "# TYPE lws_info info\n"
+			  "# HELP lws_info Version of lws producing this\n"
+			  "lws_info{version=\"%s\"} 1\n", LWS_BUILD_HASH);
+	if (lws_metrics_om_ac_stash(pss, (const char *)buf + LWS_PRE,
+				    lws_ptr_diff_size_t(p, buf + LWS_PRE)))
+		return 1;
+
+	/* system scalars */
+
+#if defined(__linux__)
+	if (grabfile("/proc/loadavg", hn, sizeof(hn))) {
+		char *sp = strchr(hn, ' ');
+		if (sp) {
+			p = start;
+			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+					  "load_1m %.*s\n",
+					  lws_ptr_diff(sp, hn), hn);
+			if (lws_metrics_om_ac_stash(pss,
+						    (char *)buf + LWS_PRE,
+						    lws_ptr_diff_size_t(p,
+								start)))
+				return 1;
+		}
+	}
+#endif
+
+	if (lws_metrics_foreach(ctx, pss, append_om_metric))
+		return 1;
+
+	p = start;
+	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
+			  "# EOF\n");
+	if (lws_metrics_om_ac_stash(pss, (char *)buf + LWS_PRE,
+				    lws_ptr_diff_size_t(p, buf + LWS_PRE)))
+		return 1;
+
+	pss->walk = pss->ac;
+
+	return 0;
+}
+
+#if defined(LWS_WITH_SERVER)
+
+/* 1) direct http export for scraper */
+
+static int
+callback_lws_openmetrics_export(struct lws *wsi,
+				enum lws_callback_reasons reason,
+				void *user, void *in, size_t len)
+{
+	unsigned char buf[1224], *start = buf + LWS_PRE, *p = start,
+		      *end = buf + sizeof(buf) - 1, *ip;
+	struct lws_context *cx = lws_get_context(wsi);
+	struct pss *pss = (struct pss *)user;
+	unsigned int m, wm;
+
+	switch (reason) {
+	case LWS_CALLBACK_HTTP:
+
+		ome_prepare(cx, pss);
+
+		p = start;
+		if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK,
+						"application/openmetrics-text; "
+						"version=1.0.0; charset=utf-8",
+						pss->tot, &p, end) ||
+		    lws_finalize_write_http_header(wsi, start, &p, end))
+			return 1;
+
+		lws_callback_on_writable(wsi);
+
+		return 0;
+
+	case LWS_CALLBACK_CLOSED_HTTP:
+		lwsac_free(&pss->ac);
+		break;
+
+	case LWS_CALLBACK_HTTP_WRITEABLE:
+		if (!pss->walk)
+			return 0;
+
+		do {
+			ip = (uint8_t *)pss->walk +
+				lwsac_sizeof(pss->walk == pss->ac) + LWS_PRE;
+			m = (unsigned int)((ip[0] << 8) | ip[1]);
+
+			/* coverity */
+			if (m > lwsac_get_tail_pos(pss->walk) -
+				lwsac_sizeof(pss->walk == pss->ac))
+				return -1;
+
+			if (lws_ptr_diff_size_t(end, p) < m)
+				break;
+
+			memcpy(p, ip + 2, m);
+			p += m;
+
+			pss->walk = lwsac_get_next(pss->walk);
+		} while (pss->walk);
+
+		if (!lws_ptr_diff_size_t(p, start)) {
+			lwsl_err("%s: stuck\n", __func__);
+			return -1;
+		}
+
+		wm = pss->walk ? LWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL;
+
+		if (lws_write(wsi, start, lws_ptr_diff_size_t(p, start),
+			      (enum lws_write_protocol)wm) < 0)
+			return 1;
+
+		if (!pss->walk) {
+			 if (lws_http_transaction_completed(wsi))
+				return -1;
+		} else
+			lws_callback_on_writable(wsi);
+
+		return 0;
+
+	default:
+		break;
+	}
+
+	return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+
+static struct pss *
+omc_lws_om_get_other_side_pss_client(struct vhd *vhd, struct pss *pss)
+{
+	/*
+	 * Search through our partner's clients list looking for one with the
+	 * same proxy path
+	 */
+	lws_start_foreach_dll(struct lws_dll2 *, d,
+			vhd->bind_partner_vhd->clients.head) {
+		struct pss *apss = lws_container_of(d, struct pss, list);
+
+		if (!strcmp(pss->proxy_path, apss->proxy_path))
+			return apss;
+
+	} lws_end_foreach_dll(d);
+
+	return NULL;
+}
+
+/* 2) "lws-openmetrics-prox-agg": http server export via proxy to connected clients */
+
+static int
+callback_lws_openmetrics_prox_agg(struct lws *wsi,
+				  enum lws_callback_reasons reason,
+				  void *user, void *in, size_t len)
+{
+	unsigned char buf[1224], *start = buf + LWS_PRE, *p = start,
+		      *end = buf + sizeof(buf) - 1, *ip;
+	struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get(
+				lws_get_vhost(wsi), lws_get_protocol(wsi));
+	struct lws_context *cx = lws_get_context(wsi);
+	struct pss *pss = (struct pss *)user, *partner_pss;
+	unsigned int m, wm;
+
+	switch (reason) {
+
+	case LWS_CALLBACK_PROTOCOL_INIT:
+		lwsl_notice("%s: PROTOCOL_INIT on %s\n", __func__, lws_vh_tag(lws_get_vhost(wsi)));
+		/*
+		 * We get told what to do when we are bound to the vhost
+		 */
+		vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
+				lws_get_protocol(wsi), sizeof(struct vhd));
+		if (!vhd) {
+			lwsl_err("%s: vhd alloc failed\n", __func__);
+			return 0;
+		}
+
+		vhd->cx = cx;
+
+		/*
+		 * Try to bind to the counterpart server in the proxy, binding
+		 * to the right one by having a common bind name set in a pvo.
+		 * We don't know who will get instantiated last, so both parts
+		 * try to bind if not already bound
+		 */
+
+		if (!lws_pvo_get_str(in, "proxy-side-bind-name",
+				     &vhd->proxy_side_bind_name)) {
+			/*
+			 * Attempt to find the vhd that belongs to a vhost
+			 * that has instantiated protocol
+			 * "lws-openmetrics-prox-server", and has set pvo
+			 * "proxy-side-bind-name" on it to whatever our
+			 * vhd->proxy_side_bind_name was also set to.
+			 *
+			 * If found, inform the two sides of the same proxy
+			 * what their partner vhd is
+			 */
+			lws_strncpy(vhd->sanity, "isagg", sizeof(vhd->sanity));
+			vhd->bind_partner_vhd = lws_vhd_find_by_pvo(cx,
+						"lws-openmetrics-prox-server",
+						"proxy-side-bind-name",
+						vhd->proxy_side_bind_name);
+			if (vhd->bind_partner_vhd) {
+				assert(!strcmp(vhd->bind_partner_vhd->sanity, "isws"));
+				lwsl_notice("%s: proxy binding OK\n", __func__);
+				vhd->bind_partner_vhd->bind_partner_vhd = vhd;
+			}
+		} else {
+			lwsl_warn("%s: proxy-side-bind-name required\n", __func__);
+			return 1;
+		}
+
+		break;
+
+	case LWS_CALLBACK_PROTOCOL_DESTROY:
+		if (vhd)
+			lws_sul_cancel(&vhd->sul);
+		break;
+
+	case LWS_CALLBACK_HTTP:
+
+		/*
+		 * The scraper has connected to us, the local side of the proxy,
+		 * we need to match what it wants to
+		 */
+
+		if (!vhd->bind_partner_vhd)
+			return 0;
+
+		lws_strnncpy(pss->proxy_path, (const char *)in, len,
+			     sizeof(pss->proxy_path));
+
+		if (pss->list.owner) {
+			lwsl_warn("%s: double HTTP?\n", __func__);
+			return 0;
+		}
+
+		pss->wsi = wsi;
+
+		lws_start_foreach_dll(struct lws_dll2 *, d,
+				      vhd->bind_partner_vhd->clients.head) {
+			struct pss *apss = lws_container_of(d, struct pss, list);
+
+			if (!strcmp((const char *)in, apss->proxy_path)) {
+				apss->trigger = 1;
+				lws_callback_on_writable(apss->wsi);
+
+				/* let's add him on the http server vhd list */
+
+				lws_dll2_add_tail(&pss->list, &vhd->clients);
+				return 0;
+			}
+
+		} lws_end_foreach_dll(d);
+
+		return 0;
+
+	case LWS_CALLBACK_CLOSED_HTTP:
+		lwsac_free(&pss->ac);
+		lws_dll2_remove(&pss->list);
+		break;
+
+	case LWS_CALLBACK_HTTP_WRITEABLE:
+
+		if (!pss->walk)
+			return 0;
+
+		/* locate the wss side if it's still around */
+
+		partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss);
+		if (!partner_pss)
+			return -1;
+
+		do {
+			ip = (uint8_t *)pss->walk +
+				lwsac_sizeof(pss->walk == partner_pss->ac) + LWS_PRE;
+			m = (unsigned int)((ip[0] << 8) | ip[1]);
+
+			/* coverity */
+			if (m > lwsac_get_tail_pos(pss->walk) -
+				lwsac_sizeof(pss->walk == partner_pss->ac))
+				return -1;
+
+			if (lws_ptr_diff_size_t(end, p) < m)
+				break;
+
+			memcpy(p, ip + 2, m);
+			p += m;
+
+			pss->walk = lwsac_get_next(pss->walk);
+		} while (pss->walk);
+
+		if (!lws_ptr_diff_size_t(p, start)) {
+			lwsl_err("%s: stuck\n", __func__);
+			return -1;
+		}
+
+		wm = pss->walk ? LWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL;
+
+		if (lws_write(wsi, start, lws_ptr_diff_size_t(p, start),
+			      (enum lws_write_protocol)wm) < 0)
+			return 1;
+
+		if (!pss->walk) {
+			lwsl_info("%s: whole msg proxied to scraper\n", __func__);
+			lws_dll2_remove(&pss->list);
+			lwsac_free(&partner_pss->ac);
+//			if (lws_http_transaction_completed(wsi))
+			return -1;
+		} else
+			lws_callback_on_writable(wsi);
+
+		return 0;
+
+	default:
+		break;
+	}
+
+	return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+
+/* 3) "lws-openmetrics-prox-server": ws server side of metrics proxy, for
+ *    ws clients to connect to */
+
+static int
+callback_lws_openmetrics_prox_server(struct lws *wsi,
+				     enum lws_callback_reasons reason,
+				     void *user, void *in, size_t len)
+{
+	unsigned char buf[1224], *start = buf + LWS_PRE, *p = start,
+		      *end = buf + sizeof(buf) - 1;
+	struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get(
+				lws_get_vhost(wsi), lws_get_protocol(wsi));
+	struct lws_context *cx = lws_get_context(wsi);
+	struct pss *pss = (struct pss *)user, *partner_pss;
+
+	switch (reason) {
+
+	case LWS_CALLBACK_PROTOCOL_INIT:
+		/*
+		 * We get told what to do when we are bound to the vhost
+		 */
+
+		lwsl_notice("%s: PROTOCOL_INIT on %s\n", __func__, lws_vh_tag(lws_get_vhost(wsi)));
+
+		vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
+				lws_get_protocol(wsi), sizeof(struct vhd));
+		if (!vhd) {
+			lwsl_err("%s: vhd alloc failed\n", __func__);
+			return 0;
+		}
+
+		vhd->cx = cx;
+
+		/*
+		 * Try to bind to the counterpart server in the proxy, binding
+		 * to the right one by having a common bind name set in a pvo.
+		 * We don't know who will get instantiated last, so both parts
+		 * try to bind if not already bound
+		 */
+
+		if (!lws_pvo_get_str(in, "proxy-side-bind-name",
+				     &vhd->proxy_side_bind_name)) {
+			/*
+			 * Attempt to find the vhd that belongs to a vhost
+			 * that has instantiated protocol
+			 * "lws-openmetrics-prox-server", and has set pvo
+			 * "proxy-side-bind-name" on it to whatever our
+			 * vhd->proxy_side_bind_name was also set to.
+			 *
+			 * If found, inform the two sides of the same proxy
+			 * what their partner vhd is
+			 */
+			lws_strncpy(vhd->sanity, "isws", sizeof(vhd->sanity));
+			vhd->bind_partner_vhd = lws_vhd_find_by_pvo(cx,
+						"lws-openmetrics-prox-agg",
+						"proxy-side-bind-name",
+						vhd->proxy_side_bind_name);
+			if (vhd->bind_partner_vhd) {
+				assert(!strcmp(vhd->bind_partner_vhd->sanity, "isagg"));
+				lwsl_notice("%s: proxy binding OK\n", __func__);
+				vhd->bind_partner_vhd->bind_partner_vhd = vhd;
+			}
+		} else {
+			lwsl_warn("%s: proxy-side-bind-name required\n", __func__);
+			return 1;
+		}
+
+		break;
+
+	case LWS_CALLBACK_PROTOCOL_DESTROY:
+		break;
+
+	case LWS_CALLBACK_ESTABLISHED:
+		/*
+		 * a client has joined... we need to add his pss to our list
+		 * of live, joined clients
+		 */
+
+		/* mark us as waiting for the reference name from the client */
+		pss->greet = 1;
+		pss->wsi = wsi;
+		lws_validity_confirmed(wsi);
+
+		return 0;
+
+	case LWS_CALLBACK_CLOSED:
+		/*
+		 * a client has parted
+		 */
+		lws_dll2_remove(&pss->list);
+		lwsl_warn("%s: client %s left (%u)\n", __func__,
+				pss->proxy_path,
+				(unsigned int)vhd->clients.count);
+		lwsac_free(&pss->ac);
+
+		/* let's kill the scraper connection accordingly, if still up */
+		partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss);
+		if (partner_pss)
+			lws_wsi_close(partner_pss->wsi, LWS_TO_KILL_ASYNC);
+		break;
+
+	case LWS_CALLBACK_RECEIVE:
+		if (pss->greet) {
+			pss->greet = 0;
+			lws_strnncpy(pss->proxy_path, (const char *)in, len,
+				     sizeof(pss->proxy_path));
+
+			lws_validity_confirmed(wsi);
+			lwsl_notice("%s: received greet '%s'\n", __func__,
+				    pss->proxy_path);
+			/*
+			 * we need to add his pss to our list of configured,
+			 * live, joined clients
+			 */
+			lws_dll2_add_tail(&pss->list, &vhd->clients);
+			return 0;
+		}
+
+		/*
+		 * He's sending us his results... let's collect chunks into the
+		 * pss lwsac before worrying about anything else
+		 */
+
+		if (lws_is_first_fragment(wsi))
+			pss->tot = 0;
+
+		lws_metrics_om_ac_stash(pss, (const char *)in, len);
+
+		if (lws_is_final_fragment(wsi)) {
+			struct pss *partner_pss;
+
+			lwsl_info("%s: ws side received complete msg\n",
+					__func__);
+
+			/* the lwsac is complete */
+			pss->walk = pss->ac;
+			partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss);
+			if (!partner_pss) {
+				lwsl_notice("%s: no partner A\n", __func__);
+				return -1;
+			}
+
+			/* indicate to scraper side we want to issue now */
+
+			p = start;
+			if (lws_add_http_common_headers(partner_pss->wsi, HTTP_STATUS_OK,
+							"application/openmetrics-text; "
+							"version=1.0.0; charset=utf-8",
+							pss->tot, &p, end) ||
+			    lws_finalize_write_http_header(partner_pss->wsi,
+							    start, &p, end))
+				return -1;
+
+			/* indicate to scraper side we want to issue now */
+
+			partner_pss->walk = pss->ac;
+			partner_pss->trigger = 1;
+			lws_callback_on_writable(partner_pss->wsi);
+		}
+
+		return 0;
+
+	case LWS_CALLBACK_SERVER_WRITEABLE:
+		if (!pss->trigger)
+			return 0;
+
+		pss->trigger = 0;
+
+		partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss);
+		if (!partner_pss) {
+			lwsl_err("%s: no partner\n", __func__);
+			return 0;
+		}
+
+		lwsl_info("%s: sending trigger to client\n", __func__);
+
+		*start = 'x';
+		if (lws_write(wsi, start, 1,
+			      (enum lws_write_protocol)LWS_WRITE_TEXT) < 0)
+			return 1;
+
+		lws_validity_confirmed(wsi);
+
+		return 0;
+
+	default:
+		break;
+	}
+
+	return lws_callback_http_dummy(wsi, reason, user, in, len);
+}
+#endif
+
+#if defined(LWS_WITH_CLIENT) && defined(LWS_ROLE_WS)
+
+/* 4) ws client that keeps wss connection up to metrics proxy ws server */
+
+static int
+callback_lws_openmetrics_prox_client(struct lws *wsi,
+				     enum lws_callback_reasons reason,
+				     void *user, void *in, size_t len)
+{
+	unsigned char buf[1224], *start = buf + LWS_PRE, *p = start,
+		      *end = buf + sizeof(buf) - 1, *ip;
+	struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get(
+				lws_get_vhost(wsi), lws_get_protocol(wsi));
+	struct lws_context *cx = lws_get_context(wsi);
+	struct pss *pss = (struct pss *)user;
+	unsigned int m, wm;
+	const char *cp;
+	char first;
+
+	switch (reason) {
+
+	case LWS_CALLBACK_PROTOCOL_INIT:
+
+		lwsl_notice("%s: PROTOCOL_INIT on %s\n", __func__,
+					lws_vh_tag(lws_get_vhost(wsi)));
+
+
+		/*
+		 * We get told what to do when we are bound to the vhost
+		 */
+		vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
+				lws_get_protocol(wsi), sizeof(struct vhd));
+		if (!vhd)
+			return 0;
+
+		vhd->cx = cx;
+		vhd->vhost = lws_get_vhost(wsi);
+
+		/* the proxy server uri */
+
+		if (lws_pvo_get_str(in, "ws-server-uri", &cp)) {
+			lwsl_err("%s: ws-server-uri pvo required\n", __func__);
+
+			return 1;
+		}
+		lws_strncpy(vhd->ws_server_uri, cp, sizeof(vhd->ws_server_uri));
+
+		/* how we should be referenced at the proxy */
+
+		if (lws_pvo_get_str(in, "metrics-proxy-path", &cp)) {
+			lwsl_err("%s: metrics-proxy-path pvo required\n", __func__);
+
+			return 1;
+		}
+		lws_strncpy(vhd->metrics_proxy_path, cp, sizeof(vhd->metrics_proxy_path));
+
+		/* the shared secret to authenticate us as allowed to join */
+
+		if (lws_pvo_get_str(in, "ba-secret", &cp)) {
+			lwsl_err("%s: ba-secret pvo required\n", __func__);
+
+			return 1;
+		}
+		lws_strncpy(vhd->ba_secret, cp, sizeof(vhd->ba_secret));
+
+		lwsl_notice("%s: scheduling connect %s %s %s\n", __func__,
+				vhd->ws_server_uri, vhd->metrics_proxy_path, vhd->ba_secret);
+
+		lws_validity_confirmed(wsi);
+		lws_sul_schedule(cx, 0, &vhd->sul, omc_connect_client, 1);
+		break;
+
+	case LWS_CALLBACK_PROTOCOL_DESTROY:
+		if (vhd)
+			lws_sul_cancel(&vhd->sul);
+		break;
+
+	case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
+	{
+		unsigned char **pp = (unsigned char **)in, *pend = (*pp) + len;
+		char b[128];
+
+		/* authorize ourselves to the metrics proxy using basic auth */
+
+		if (lws_http_basic_auth_gen("metricsclient", vhd->ba_secret,
+					    b, sizeof(b)))
+			break;
+
+		if (lws_add_http_header_by_token(wsi,
+						 WSI_TOKEN_HTTP_AUTHORIZATION,
+						 (unsigned char *)b,
+						 (int)strlen(b), pp, pend))
+			return -1;
+
+		break;
+	}
+
+	case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
+		lwsl_err("CLIENT_CONNECTION_ERROR: %s\n",
+			 in ? (char *)in : "(null)");
+		goto do_retry;
+
+	case LWS_CALLBACK_CLIENT_ESTABLISHED:
+		lwsl_warn("%s: connected to ws metrics agg server\n", __func__);
+		pss->greet = 1;
+		lws_callback_on_writable(wsi);
+		lws_validity_confirmed(wsi);
+		return 0;
+
+	case LWS_CALLBACK_CLIENT_CLOSED:
+		lwsl_notice("%s: client closed\n", __func__);
+		lwsac_free(&pss->ac);
+		goto do_retry;
+
+	case LWS_CALLBACK_CLIENT_RECEIVE:
+		/*
+		 * Proxy serverside sends us something to trigger us to create
+		 * our metrics message and send it back over the ws link
+		 */
+		ome_prepare(cx, pss);
+		pss->walk = pss->ac;
+		lws_callback_on_writable(wsi);
+		lwsl_info("%s: dump requested\n", __func__);
+		break;
+
+	case LWS_CALLBACK_CLIENT_WRITEABLE:
+		if (pss->greet) {
+			/*
+			 * At first after establishing the we link, we send a
+			 * message indicating to the metrics proxy how we
+			 * should be referred to by the scraper to particularly
+			 * select to talk to us
+			 */
+			lwsl_info("%s: sending greet '%s'\n", __func__,
+					vhd->metrics_proxy_path);
+			lws_strncpy((char *)start, vhd->metrics_proxy_path,
+					sizeof(buf) - LWS_PRE);
+			if (lws_write(wsi, start,
+				      strlen(vhd->metrics_proxy_path),
+				      LWS_WRITE_TEXT) < 0)
+				return 1;
+
+			lws_validity_confirmed(wsi);
+
+			pss->greet = 0;
+			return 0;
+		}
+
+		if (!pss->walk)
+			return 0;
+
+		/*
+		 * We send the metrics dump in a single logical ws message,
+		 * using ws fragmentation to split it around 1 mtu boundary
+		 * and keep coming back until it's finished
+		 */
+
+		first = pss->walk == pss->ac;
+
+		do {
+			ip = (uint8_t *)pss->walk +
+				lwsac_sizeof(pss->walk == pss->ac) + LWS_PRE;
+			m = (unsigned int)((ip[0] << 8) | ip[1]);
+
+			/* coverity */
+			if (m > lwsac_get_tail_pos(pss->walk) -
+				lwsac_sizeof(pss->walk == pss->ac)) {
+				lwsl_err("%s: size blow\n", __func__);
+				return -1;
+			}
+
+			if (lws_ptr_diff_size_t(end, p) < m)
+				break;
+
+			memcpy(p, ip + 2, m);
+			p += m;
+
+			pss->walk = lwsac_get_next(pss->walk);
+		} while (pss->walk);
+
+		if (!lws_ptr_diff_size_t(p, start)) {
+			lwsl_err("%s: stuck\n", __func__);
+			return -1;
+		}
+
+		wm = (unsigned int)lws_write_ws_flags(LWS_WRITE_TEXT, first,
+						      !pss->walk);
+
+		if (lws_write(wsi, start, lws_ptr_diff_size_t(p, start),
+			      (enum lws_write_protocol)wm) < 0) {
+			lwsl_notice("%s: write fail\n", __func__);
+			return 1;
+		}
+
+		lws_validity_confirmed(wsi);
+		lwsl_info("%s: forwarded %d\n", __func__, lws_ptr_diff(p, start));
+
+		if (!pss->walk) {
+			lwsl_info("%s: dump send completed\n", __func__);
+			lwsac_free(&pss->ac);
+		} else
+			lws_callback_on_writable(wsi);
+
+		return 0;
+
+	default:
+		break;
+	}
+
+	return lws_callback_http_dummy(wsi, reason, user, in, len);
+
+do_retry:
+	if (!lws_retry_sul_schedule(cx, 0, &vhd->sul, &retry,
+				    omc_connect_client, &vhd->retry_count))
+		return 0;
+
+	vhd->retry_count = 0;
+	lws_retry_sul_schedule(cx, 0, &vhd->sul, &retry,
+			       omc_connect_client, &vhd->retry_count);
+
+	return 0;
+}
+#endif
+
+
+LWS_VISIBLE const struct lws_protocols lws_openmetrics_export_protocols[] = {
+#if defined(LWS_WITH_SERVER)
+	{ /* for scraper directly: http export on listen socket */
+		"lws-openmetrics",
+		callback_lws_openmetrics_export,
+		sizeof(struct pss),
+		1024,
+	},
+	{ /* for scraper via ws proxy: http export on listen socket */
+		"lws-openmetrics-prox-agg",
+		callback_lws_openmetrics_prox_agg,
+		sizeof(struct pss),
+		1024,
+	},
+	{ /* metrics proxy server side: ws server for clients to connect to */
+		"lws-openmetrics-prox-server",
+		callback_lws_openmetrics_prox_server,
+		sizeof(struct pss),
+		1024,
+	},
+#endif
+#if defined(LWS_WITH_CLIENT) && defined(LWS_ROLE_WS)
+	{ /* client to metrics proxy: ws client to connect to metrics proxy*/
+		"lws-openmetrics-prox-client",
+		callback_lws_openmetrics_prox_client,
+		sizeof(struct pss),
+		1024,
+	},
+#endif
+};
+
+LWS_VISIBLE const lws_plugin_protocol_t lws_openmetrics_export = {
+	.hdr = {
+		"lws OpenMetrics export",
+		"lws_protocol_plugin",
+		LWS_BUILD_HASH,
+		LWS_PLUGIN_API_MAGIC
+	},
+
+	.protocols = lws_openmetrics_export_protocols,
+	.count_protocols = LWS_ARRAY_SIZE(lws_openmetrics_export_protocols),
+};
diff --git a/plugins/protocol_lws_server_status.c b/plugins/protocol_lws_server_status.c
deleted file mode 100644
index 497f59efd11239496dd76c3961b4f9ef99e8c9c8..0000000000000000000000000000000000000000
--- a/plugins/protocol_lws_server_status.c
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * libwebsockets-test-server - libwebsockets test implementation
- *
- * Written in 2010-2019 by Andy Green <andy@warmcat.com>
- *
- * This file is made available under the Creative Commons CC0 1.0
- * Universal Public Domain Dedication.
- *
- * The person who associated a work with this deed has dedicated
- * the work to the public domain by waiving all of his or her rights
- * to the work worldwide under copyright law, including all related
- * and neighboring rights, to the extent allowed by law. You can copy,
- * modify, distribute and perform the work, even for commercial purposes,
- * all without asking permission.
- *
- * The test apps are intended to be adapted for use in your code, which
- * may be proprietary.  So unlike the library itself, they are licensed
- * Public Domain.
- */
-
-#define LWS_DLL
-#define LWS_INTERNAL
-#include <libwebsockets.h>
-#include <string.h>
-#include <stdlib.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-#include <stdio.h>
-
-struct lws_ss_filepath {
-	struct lws_ss_filepath *next;
-	char filepath[128];
-};
-
-struct lws_ss_dumps {
-	char buf[32768];
-	int length;
-};
-
-struct pss {
-	int ver;
-	int pos;
-};
-
-struct vhd {
-	struct lws_context *context;
-	struct lws_vhost *vhost;
-	const struct lws_protocols *protocol;
-	lws_sorted_usec_list_t sul;
-	int hide_vhosts;
-	int tow_flag;
-	int period_s;
-	int clients;
-	struct lws_ss_dumps d;
-	struct lws_ss_filepath *fp;
-};
-
-static const struct lws_protocols protocols[1];
-
-static void
-update(struct lws_sorted_usec_list *sul)
-{
-	struct vhd *v = lws_container_of(sul, struct vhd, sul);
-	struct lws_ss_filepath *fp;
-	char contents[256], pure[256], *p = v->d.buf + LWS_PRE,
-	     *end = v->d.buf + sizeof(v->d.buf) - LWS_PRE - 1;
-	int n, first = 1, fd;
-
-	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "{\"i\":");
-	p += lws_json_dump_context(v->context, p, lws_ptr_diff(end, p),
-				   v->hide_vhosts);
-	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ", \"files\": [");
-
-	fp = v->fp;
-	while (fp) {
-		if (!first)
-			p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ",");
-
-		strcpy(pure, "(unknown)");
-		fd = lws_open(fp->filepath, LWS_O_RDONLY);
-		if (fd >= 0) {
-			n = (int)read(fd, contents, sizeof(contents) - 1);
-			close(fd);
-			if (n >= 0) {
-				contents[n] = '\0';
-				lws_json_purify(pure, contents, sizeof(pure), NULL);
-			}
-		}
-
-		p += lws_snprintf(p, lws_ptr_diff_size_t(end, p),
-				"{\"path\":\"%s\",\"val\":\"%s\"}",
-					fp->filepath, pure);
-		first = 0;
-
-		fp = fp->next;
-	}
-	p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "]}");
-	v->d.length = lws_ptr_diff(p, (v->d.buf + LWS_PRE));
-
-	lws_callback_on_writable_all_protocol(v->context, &protocols[0]);
-
-	lws_sul_schedule(v->context, 0, &v->sul, update, v->period_s * LWS_US_PER_SEC);
-}
-
-static int
-callback_lws_server_status(struct lws *wsi, enum lws_callback_reasons reason,
-			   void *user, void *in, size_t len)
-{
-	const struct lws_protocol_vhost_options *pvo =
-			(const struct lws_protocol_vhost_options *)in;
-	struct vhd *v = (struct vhd *)
-			lws_protocol_vh_priv_get(lws_get_vhost(wsi),
-					lws_get_protocol(wsi));
-	struct lws_ss_filepath *fp, *fp1, **fp_old;
-	int m;
-
-	switch (reason) {
-
-	case LWS_CALLBACK_ESTABLISHED:
-		lwsl_info("%s: LWS_CALLBACK_ESTABLISHED\n", __func__);
-		if (!v->clients++) {
-			lws_sul_schedule(lws_get_context(wsi), 0, &v->sul, update, 1);
-			lwsl_info("%s: starting updates\n", __func__);
-		}
-		break;
-
-	case LWS_CALLBACK_CLOSED:
-		if (!--v->clients)
-			lwsl_notice("%s: stopping updates\n", __func__);
-
-		break;
-
-	case LWS_CALLBACK_PROTOCOL_INIT: /* per vhost */
-		if (v)
-			break;
-
-		lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
-					    lws_get_protocol(wsi),
-					    sizeof(struct vhd));
-		v = (struct vhd *)lws_protocol_vh_priv_get(lws_get_vhost(wsi),
-							   lws_get_protocol(wsi));
-
-		fp_old = &v->fp;
-
-		while (pvo) {
-			if (!strcmp(pvo->name, "hide-vhosts"))
-				v->hide_vhosts = atoi(pvo->value);
-			if (!strcmp(pvo->name, "update-ms"))
-				v->period_s = (atoi(pvo->value) + 500) / 1000;
-			else
-				v->period_s = 5;
-			if (!strcmp(pvo->name, "filepath")) {
-				fp = malloc(sizeof(*fp));
-				if (!fp)
-					return -1;
-				fp->next = NULL;
-				lws_snprintf(&fp->filepath[0],
-					     sizeof(fp->filepath), "%s",
-					     pvo->value);
-				*fp_old = fp;
-				fp_old = &fp->next;
-			}
-			pvo = pvo->next;
-		}
-		v->context = lws_get_context(wsi);
-		v->vhost = lws_get_vhost(wsi);
-		v->protocol = lws_get_protocol(wsi);
-
-		lws_sul_schedule(lws_get_context(wsi), 0, &v->sul, update, 1);
-		break;
-
-	case LWS_CALLBACK_PROTOCOL_DESTROY: /* per vhost */
-		if (!v)
-			break;
-		fp = v->fp;
-		while (fp) {
-			fp1= fp->next;
-			free(fp);
-			fp = fp1;
-		}
-		break;
-
-	case LWS_CALLBACK_SERVER_WRITEABLE:
-		m = lws_write(wsi, (unsigned char *)v->d.buf + LWS_PRE,
-			      (size_t)v->d.length, LWS_WRITE_TEXT);
-		if (m < 0)
-			return -1;
-		break;
-
-	default:
-		break;
-	}
-
-	return 0;
-}
-
-static const struct lws_protocols protocols[] = {
-	{
-		"lws-server-status",
-		callback_lws_server_status,
-		sizeof(struct pss),
-		1024,
-	},
-};
-
-LWS_VISIBLE const lws_plugin_protocol_t lws_server_status = {
-	.hdr = {
-		"lws server status",
-		"lws_protocol_plugin",
-		LWS_PLUGIN_API_MAGIC
-	},
-
-	.protocols = protocols,
-	.count_protocols = LWS_ARRAY_SIZE(protocols),
-	.extensions = NULL,
-	.count_extensions = 0,
-};
diff --git a/plugins/server-status.html b/plugins/server-status.html
deleted file mode 100644
index 0c03863353e330c3ff6ff6b88d0d6d546f64db5d..0000000000000000000000000000000000000000
--- a/plugins/server-status.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset=utf-8 http-equiv="Content-Language" content="en"/>
- <link rel="stylesheet" type="text/css" href="server-status.css"/>
- <script src="/lws-common.js"></script>
- <script type='text/javascript' src='server-status.js'></script>
- <title>LWS Server Status</title>
-</head>
-
-<body>
-<header></header>
-<article>
-
-<table>
-<tr><td><img src="./lwsws-logo.png"></td><td>
-<span id=title class=title>Server status</span></td></tr>
-<tr><td align=center colspan=2>
-<div id="conninfo">...</div>
-<div id="json"></div>
-</td></tr>
-</table>
-</article>
-</body>
-</html>
diff --git a/plugins/server-status.js b/plugins/server-status.js
deleted file mode 100644
index ddc7796df23b1dfc3923b521e789819b6381b95c..0000000000000000000000000000000000000000
--- a/plugins/server-status.js
+++ /dev/null
@@ -1,249 +0,0 @@
-(function() {
-
-/*
- * We display untrusted stuff in html context... reject anything
- * that has HTML stuff in it
- */
-
-function san(s)
-{
-	if (s.search("<") !== -1)
-		return "invalid string";
-	
-	return s;
-}
-
-function humanize(s)
-{
-	var i = parseInt(s, 10);
-	
-	if (i >= (1024 * 1024 * 1024))
-		return (i / (1024 * 1024 * 1024)).toFixed(3) + "Gi";
-	
-	if (i >= (1024 * 1024))
-		return (i / (1024 * 1024)).toFixed(3) + "Mi";
-	
-	if (i > 1024)
-		return (i / 1024).toFixed(3) + "Ki";
-	
-	return s;
-}
-
-function get_appropriate_ws_url()
-{
-	var pcol;
-	var u = document.URL;
-
-	/*
-	 * We open the websocket encrypted if this page came on an
-	 * https:// url itself, otherwise unencrypted
-	 */
-
-	if (u.substring(0, 5) === "https") {
-		pcol = "wss://";
-		u = u.substr(8);
-	} else {
-		pcol = "ws://";
-		if (u.substring(0, 4) === "http")
-			u = u.substr(7);
-	}
-
-	u = u.split("/");
-
-	/* + "/xxx" bit is for IE10 workaround */
-
-	return pcol + u[0] + "/xxx";
-}
-
-
-	var socket_status, jso, s;
-
-function ws_open_server_status()
-{	
-	socket_status = new WebSocket(get_appropriate_ws_url(),
-				   "lws-server-status");
-
-	try {
-		socket_status.onopen = function() {
-			document.getElementById("title").innerHTML = "Server Status (Active)";
-			lws_gray_out(false);
-		};
-
-		socket_status.onmessage =function got_packet(msg) {
-			var u, ci, n;
-			// document.getElementById("json").innerHTML = "<pre>"+msg.data+"</pre>";
-			if (msg.data.length < 100)
-				return;
-			jso = JSON.parse(msg.data);
-			u = parseInt(san(jso.i.uptime), 10);
-
-			if (parseInt(jso.i.contexts[0].deprecated, 10) === 0)
-				s = "<table><tr><td></td><td class=\"c0\">";
-			else
-				s = "<table><tr><td></td><td class=\"dc0\">";
-			s +=
-			  "Server</td><td>" +
-			  "<span class=\"sn\">Server Version:</span> <span class=\"v\">" +
-			   san(jso.i.version) + "</span><br>" +
-			  "<span class=\"sn\">Host Uptime:</span> <span class=\"v\">" +
-			  ((u / (24 * 3600)) | 0) + "d " +
-			  (((u % (24 * 3600)) / 3600) | 0) + "h " +
-			  (((u % 3600) / 60) | 0) + "m</span>";
-			if (jso.i.l1)
-				s = s + ", <span class=\"sn\">Host Load:</span> <span class=\"v\">" + san(jso.i.l1) + " ";
-			if (jso.i.l2)
-				s = s + san(jso.i.l2) + " ";
-			if (jso.i.l3)
-				s = s + san(jso.i.l3);
-			if (jso.i.l1)
-				s =s + "</span>";
-				
-			if (jso.i.statm) {
-				var sm = jso.i.statm.split(" ");
-				s += ", <span class=\"sn\">Virt stack + heap Usage:</span> <span class=\"v\">" +
-					humanize(parseInt(sm[5], 10) * 4096) + "B</span>";
-			}
-			s += ", <span class=\"sn\">lws heap usage:</span> <span class=\"v\">" +
-			humanize(jso.i.heap) + "B</span>";
-
-				
-			for (n = 0; n < jso.files.length; n++) {
-				s += "<br><span class=n>" + san(jso.files[n].path) + ":</span><br>    " + san(jso.files[n].val);
-			}
-			s += "</td></tr>";
-
-			for (ci = 0; ci < jso.i.contexts.length; ci++) {
-
-				if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0)
-					s += "<tr><td></td><td class=\"c\">" +
-					  "Active Context</td><td>";
-				else
-					s += "<tr><td></td><td class=\"c1\">" +
-					  "Deprecated Context " + ci + "</td><td>";
-
-				  u = parseInt(san(jso.i.contexts[ci].context_uptime), 10);
-	  			  s += "<span class=n>Server Uptime:</span> <span class=v>" +
-				  ((u / (24 * 3600)) | 0) + "d " +
-				  (((u % (24 * 3600)) / 3600) | 0) + "h " +
-				  (((u % 3600) / 60) | 0) + "m</span>";
-
-				s = s +
-				  "<br>" +
-				  "<span class=n>Tagged objects alive:</span> <span class=v>" + san(jso.i.contexts[ci].wsi_alive) + "</span><br>" +
-			  	  "<span class=n>Total Rx:</span> <span class=v>" + humanize(san(jso.i.contexts[ci].rx)) +"B</span>, " +
-			  	  "<span class=n>Total Tx:</span> <span class=v>" + humanize(san(jso.i.contexts[ci].tx)) +"B</span><br>" +
-			  	  
-			  	  "<span class=n>CONNECTIONS: HTTP/1.x:</span> <span class=v>" + san(jso.i.contexts[ci].h1_conn) +"</span>, " +
-			  	  "<span class=n>Websocket:</span> <span class=v>" + san(jso.i.contexts[ci].ws_upg) +"</span>, " +
-			  	  "<span class=n>H2 upgrade:</span> <span class=v>" + san(jso.i.contexts[ci].h2_upg) +"</span>, " +
-			  	  "<span class=n>H2 ALPN:</span> <span class=v>" + san(jso.i.contexts[ci].h2_alpn) +"</span>, " +
-			  	  "<span class=n>Rejected:</span> <span class=v>" + san(jso.i.contexts[ci].rejected) +"</span><br>" +
-
-			  	  "<span class=n>TRANSACTIONS: HTTP/1.x:</span> <span class=v>" + san(jso.i.contexts[ci].h1_trans) + "</span>, " +
-			  	  "<span class=n>H2:</span> <span class=v>" + san(jso.i.contexts[ci].h2_trans) +"</span>, " +
-			  	   "<span class=n>Total H2 substreams:</span> <span class=v>" + san(jso.i.contexts[ci].h2_subs) +"</span><br>" +
-
-				  "<span class=n>CGI: alive:</span> <span class=v>" + san(jso.i.contexts[ci].cgi_alive) + "</span>, " +
-				  "<span class=n>spawned:</span> <span class=v>" + san(jso.i.contexts[ci].cgi_spawned) +
-				  "</span><table>";
-				
-				for (n = 0; n < jso.i.contexts[ci].pt.length; n++) {
-
-					if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0)
-						s += "<tr><td>&nbsp;&nbsp;</td><td class=\"l\">service thread " + (n + 1);
-					else
-						s += "<tr><td>&nbsp;&nbsp;</td><td class=\"dl\">service thread " + (n + 1);
-					s += "</td><td>" +
-					"<span class=n>fds:</span> <span class=v>" + san(jso.i.contexts[ci].pt[n].fds_count) + " / " +
-						  san(jso.i.contexts[ci].pt_fd_max) + "</span>, ";
-					s = s + "<span class=n>ah pool:</span> <span class=v>" + san(jso.i.contexts[ci].pt[n].ah_pool_inuse) + " / " +
-						      san(jso.i.contexts[ci].ah_pool_max) + "</span>, " +
-					"<span class=n>ah waiting list:</span> <span class=v>" + san(jso.i.contexts[ci].pt[n].ah_wait_list);
-	
-					s = s + "</span></td></tr>";
-	
-				}
-				for (n = 0; n < jso.i.contexts[ci].vhosts.length; n++) {
-					if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0)
-						s += "<tr><td>&nbsp;&nbsp;</td><td class=\"l\">vhost " + (n + 1);
-					else
-						s += "<tr><td>&nbsp;&nbsp;</td><td class=\"dl\">vhost " + (n + 1);
-					s += "</td><td><span class=\"mountname\">";
-					if (jso.i.contexts[ci].vhosts[n].use_ssl === "1")
-						s = s + "https://";
-					else
-						s = s + "http://";
-					s = s + san(jso.i.contexts[ci].vhosts[n].name) + ":" +
-						san(jso.i.contexts[ci].vhosts[n].port) + "</span>";
-					if (jso.i.contexts[ci].vhosts[n].sts === "1")
-						s = s + " (STS)";
-					s = s +"<br>" +
-					
-					  "<span class=n>Total Rx:</span> <span class=v>" + humanize(san(jso.i.contexts[ci].vhosts[n].rx)) +"B</span>, " +
-					  "<span class=n>Total Tx:</span> <span class=v>" + humanize(san(jso.i.contexts[ci].vhosts[n].tx)) +"B</span><br>" +
-					  
-					  "<span class=n>CONNECTIONS: HTTP/1.x:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].h1_conn) +"</span>, " +
-					  "<span class=n>Websocket:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].ws_upg) +"</span>, " +
-					  "<span class=n>H2 upgrade:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].h2_upg) +"</span>, " +
-					  "<span class=n>H2 ALPN:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].h2_alpn) +"</span>, " +
-					  "<span class=n>Rejected:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].rejected) +"</span><br>" +
-					
-					  "<span class=n>TRANSACTIONS: HTTP/1.x:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].h1_trans) + "</span>, " +
-					  "<span class=n>H2:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].h2_trans) +"</span>, " +
-					  "<span class=n>Total H2 substreams:</span> <span class=v>" + san(jso.i.contexts[ci].vhosts[n].h2_subs) +"</span><br>";
-					
-					if (jso.i.contexts[ci].vhosts[n].mounts) {
-						s = s + "<table><tr><td class=t>Mountpoint</td><td class=t>Origin</td><td class=t>Cache Policy</td></tr>";
-	
-						var m;
-						for (m = 0; m < jso.i.contexts[ci].vhosts[n].mounts.length; m++) {
-							s = s + "<tr><td>";
-							s = s + "<span class=\"m1\">" + san(jso.i.contexts[ci].vhosts[n].mounts[m].mountpoint) +
-								"</span></td><td><span class=\"m2\">" +
-								san(jso.i.contexts[ci].vhosts[n].mounts[m].origin) +
-								"</span></td><td>";
-							if (parseInt(san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_max_age), 10))
-								s = s + "<span class=n>max-age:</span> <span class=v>" +
-								san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_max_age) +
-								"</span>, <span class=n>reuse:</span> <span class=v>" +
-								san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_reuse) +
-								"</span>, <span class=n>reval:</span> <span class=v>" +
-								san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_revalidate) +
-								"</span>, <span class=n>inter:</span> <span class=v>" +
-								san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_intermediaries);
-							s = s + "</span></td></tr>";
-						}
-						s = s + "</table>";
-					}
-					s = s + "</td></tr>";
-				}
-
-				s += "</table></td></tr>";
-				
-			} // context
-			s = s + "</table>";
-			
-			document.getElementById("conninfo").innerHTML = s;
-		};
-
-		socket_status.onclose = function(){
-			document.getElementById("title").innerHTML = "Server Status (Disconnected)";
-			lws_gray_out(true,{"zindex":"499"});
-		};
-	} catch(exception) {
-		alert("<p>Error" + exception);  
-	}
-}
-
-/* stuff that has to be delayed until all the page assets are loaded */
-
-window.addEventListener("load", function() {
-
-	lws_gray_out(true,{"zindex":"499"});
-	
-	ws_open_server_status();
-	
-}, false);
-
-}());
-