diff --git a/changelog.d/5458.feature b/changelog.d/5458.feature
new file mode 100644
index 000000000..9497f521c
--- /dev/null
+++ b/changelog.d/5458.feature
@@ -0,0 +1,1 @@
+Statistics on forward extremities per room are now exposed via Prometheus.
diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py
index 539c35352..0d3ae1a43 100644
--- a/synapse/metrics/__init__.py
+++ b/synapse/metrics/__init__.py
@@ -227,7 +227,7 @@ class BucketCollector(object):
                     break
 
         for i in self.buckets:
-            res.append([i, buckets.get(i, 0)])
+            res.append([str(i), buckets.get(i, 0)])
 
         res.append(["+Inf", sum(data.values())])
 

New extremities histogram metric breaks metrics
Error is:

```
AttributeError: 'float' object has no attribute 'replace'
  File "twisted/web/server.py", line 199, in process
    self.render(resrc)
  File "synapse/http/site.py", line 131, in render
    Request.render(self, resrc)
  File "twisted/web/server.py", line 259, in render
    body = resrc.render(self)
  File "twisted/web/resource.py", line 250, in render
    return m(request)
  File "prometheus_client/twisted/_exposition.py", line 18, in render_GET
    return generate_latest(self.registry)
  File "prometheus_client/exposition.py", line 78, in generate_latest
    for k, v in sorted(labels.items())]))
  File "prometheus_client/exposition.py", line 78, in <listcomp>
    for k, v in sorted(labels.items())]))
```

Local variables at the time: `k = 'le'`, `v = 1.0`
diff --git a/tests/storage/test_event_metrics.py b/tests/storage/test_event_metrics.py
index 20a068f1f..1655fcdaf 100644
--- a/tests/storage/test_event_metrics.py
+++ b/tests/storage/test_event_metrics.py
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from prometheus_client.exposition import generate_latest
+
 from synapse.metrics import REGISTRY
 from synapse.types import Requester, UserID
 
@@ -52,46 +54,29 @@ class ExtremStatisticsTestCase(HomeserverTestCase):
         self.reactor.advance(60 * 60 * 1000)
         self.pump(1)
 
-        items = list(
+        items = set(
             filter(
-                lambda x: x.name == "synapse_forward_extremities",
-                list(REGISTRY.collect()),
+                lambda x: b"synapse_forward_extremities_" in x,
+                generate_latest(REGISTRY).split(b"\n"),
             )
         )
 
-        # Check the values are what we want
-        buckets = {}
-        _count = 0
-        _sum = 0
-
-        for i in items[0].samples:
-            if i[0].endswith("_bucket"):
-                buckets[i[1]['le']] = i[2]
-            elif i[0].endswith("_count"):
-                _count = i[2]
-            elif i[0].endswith("_sum"):
-                _sum = i[2]
+        expected = set([
+            b'synapse_forward_extremities_bucket{le="1.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="2.0"} 2.0',
+            b'synapse_forward_extremities_bucket{le="3.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="5.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="7.0"} 1.0',
+            b'synapse_forward_extremities_bucket{le="10.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="15.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="20.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="50.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="100.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="200.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="500.0"} 0.0',
+            b'synapse_forward_extremities_bucket{le="+Inf"} 3.0',
+            b'synapse_forward_extremities_count 3.0',
+            b'synapse_forward_extremities_sum 10.0',
+        ])
 
-        # 3 buckets, 2 with 2 extrems, 1 with 6 extrems (bucketed as 7), and
-        # +Inf which is all
-        self.assertEqual(
-            buckets,
-            {
-                1.0: 0,
-                2.0: 2,
-                3.0: 0,
-                5.0: 0,
-                7.0: 1,
-                10.0: 0,
-                15.0: 0,
-                20.0: 0,
-                50.0: 0,
-                100.0: 0,
-                200.0: 0,
-                500.0: 0,
-                "+Inf": 3,
-            },
-        )
-        # 3 rooms, with 10 total events
-        self.assertEqual(_count, 3)
-        self.assertEqual(_sum, 10)
+        self.assertEqual(items, expected)

pip install -e .[all]
parse_log_pytest
pytest
pytest coverage coverage-enable-subprocess
apt-get update apt-get install -y build-essential python3-dev libffi-dev libssl-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
acme==0.40.1
aiocontextvars==0.2.2
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
Automat==22.10.0
bcrypt==4.0.1
bleach==4.1.0
boltons==23.1.1
canonicaljson==1.5.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
constantly==15.1.0
contextvars==2.4
coverage==6.2
coverage-enable-subprocess==1.0
cryptography==40.0.2
daemonize==2.5.0
defusedxml==0.7.1
elementpath==2.4.0
eliot==1.14.0
frozendict==2.4.6
hyperlink==21.0.0
idna==3.10
immutables==0.19
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
importlib-resources==5.4.0
incremental==22.10.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.0.3
josepy==1.13.0
jsonschema==3.2.0
ldap3==2.9.1
lxml==5.3.1
MarkupSafe==2.0.1
-e git+https://github.com/matrix-org/synapse.git@b59a4eba644d123fce03809ead2121e9e0da6645#egg=matrix_synapse
matrix-synapse-ldap3==0.1.5
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
msgpack==1.0.5
netaddr==0.10.1
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
parameterized==0.8.1
pem==21.2.0
phonenumbers==9.0.2
Pillow==8.4.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
prometheus-client==0.3.1
psutil==7.0.0
psycopg2==2.7.7
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycparser==2.21
pymacaroons==0.13.0
PyNaCl==1.5.0
pyOpenSSL==23.2.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pyRFC3339==2.0.1
pyrsistent==0.18.0
pysaml2==7.3.1
pytest==6.2.4
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.1
requests==2.27.1
requests-toolbelt==1.0.0
sentry-sdk==2.24.1
service-identity==21.1.0
signedjson==1.1.4
simplejson==3.20.1
six==1.17.0
sortedcontainers==2.4.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
treq==22.2.0
Twisted==22.4.0
txacme==0.9.3
TxSNI==0.2.0
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
unpaddedbase64==2.1.0
urllib3==1.26.20
webencodings==0.5.1
xmlschema==1.9.2
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work
zope.interface==5.5.2

name: synapse
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - acme==0.40.1
      - aiocontextvars==0.2.2
      - automat==22.10.0
      - bcrypt==4.0.1
      - bleach==4.1.0
      - boltons==23.1.1
      - canonicaljson==1.5.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - constantly==15.1.0
      - contextvars==2.4
      - coverage==6.2
      - coverage-enable-subprocess==1.0
      - cryptography==40.0.2
      - daemonize==2.5.0
      - defusedxml==0.7.1
      - elementpath==2.4.0
      - eliot==1.14.0
      - frozendict==2.4.6
      - hyperlink==21.0.0
      - idna==3.10
      - immutables==0.19
      - importlib-resources==5.4.0
      - incremental==22.10.0
      - jinja2==3.0.3
      - josepy==1.13.0
      - jsonschema==3.2.0
      - ldap3==2.9.1
      - lxml==5.3.1
      - markupsafe==2.0.1
      - matrix-synapse-ldap3==0.1.5
      - mock==5.2.0
      - msgpack==1.0.5
      - netaddr==0.10.1
      - parameterized==0.8.1
      - pem==21.2.0
      - phonenumbers==9.0.2
      - pillow==8.4.0
      - prometheus-client==0.3.1
      - psutil==7.0.0
      - psycopg2==2.7.7
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pycparser==2.21
      - pymacaroons==0.13.0
      - pynacl==1.5.0
      - pyopenssl==23.2.0
      - pyrfc3339==2.0.1
      - pyrsistent==0.18.0
      - pysaml2==7.3.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - sentry-sdk==2.24.1
      - service-identity==21.1.0
      - signedjson==1.1.4
      - simplejson==3.20.1
      - six==1.17.0
      - sortedcontainers==2.4.0
      - treq==22.2.0
      - twisted==22.4.0
      - txacme==0.9.3
      - txsni==0.2.0
      - unpaddedbase64==2.1.0
      - urllib3==1.26.20
      - webencodings==0.5.1
      - xmlschema==1.9.2
      - zope-interface==5.5.2
prefix: /opt/conda/envs/synapse

tests/storage/test_event_metrics.py::ExtremStatisticsTestCase::test_exposed_to_prometheus
synapse/metrics/__init__.py changelog.d/5458.feature
synapse/metrics/__init__.py changelog.d/5458.feature