diff --git a/README.md b/README.md
index 02bccb17ad92d5612fcb9693befcc12da82db86a..75d384c066d3c693fa6c66817b8c7dec8a59f967 100644
--- a/README.md
+++ b/README.md
@@ -132,12 +132,14 @@ SVG or JSON output. When deliberately changing the output, run
 `SNAPSHOT_DRY=1 npm run test:js:server` to preview changes to the saved
 snapshots, and `SNAPSHOT_UPDATE=1 npm run test:js:server` to update them.
 
-The server can be [configured][sentry configuration] to use [Sentry][sentry].
+The server can be configured to use [Sentry][] ([configuration][sentry configuration]) and [Prometheus][] ([configuration][prometheus configuration]).
 
 [package manager]: https://nodejs.org/en/download/package-manager/
 [snapshot tests]: https://glebbahmutov.com/blog/snapshot-testing/
-[sentry configuration]: doc/self-hosting.md#sentry
+[Prometheus]: https://prometheus.io/
+[prometheus configuration]: doc/self-hosting.md#prometheus
 [Sentry]: https://sentry.io/
+[sentry configuration]: doc/self-hosting.md#sentry
 
 Hosting your own server
 -----------------------
diff --git a/doc/self-hosting.md b/doc/self-hosting.md
index b35293b90ef705c561c744166bcb53c6770960b9..742681c1f15373a5ea6997c0b42ba1d1df96d3b9 100644
--- a/doc/self-hosting.md
+++ b/doc/self-hosting.md
@@ -205,3 +205,11 @@ sudo SENTRY_DSN=https://xxx:yyy@sentry.io/zzz node server
 ```
 sudo node server
 ```
+
+### Prometheus
+Shields uses [prom-client](https://github.com/siimon/prom-client) to provide [default metrics](https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors). These metrics are disabled by default.
+You can enable them by `METRICS_PROMETHEUS_ENABLED` environment variable. Moreover access to metrics resource is blocked for requests from any IP address by default. You can provide a regular expression with allowed IP addresses by `METRICS_PROMETHEUS_ALLOWED_IPS` environment variable.
+```bash
+METRICS_PROMETHEUS_ENABLED=true METRICS_PROMETHEUS_ALLOWED_IPS="^127\.0\.0\.1$" npm start
+```
+Metrics are available at `/metrics` resource.
diff --git a/lib/server-config.js b/lib/server-config.js
index 1a5ce8dabcc93bc927ec0b1bf3ffa66725706dbb..90ebec8fb26588a9d6a2e177298ce74da9fcfa51 100644
--- a/lib/server-config.js
+++ b/lib/server-config.js
@@ -39,6 +39,12 @@ const config = {
     port,
     address,
   },
+  metrics: {
+    prometheus: {
+      enabled: envFlag(process.env.METRICS_PROMETHEUS_ENABLED, false),
+      allowedIps: process.env.METRICS_PROMETHEUS_ALLOWED_IPS,
+    },
+  },
   ssl: {
     isSecure,
     key: process.env.HTTPS_KEY,
diff --git a/lib/sys/prometheus-metrics.js b/lib/sys/prometheus-metrics.js
new file mode 100644
index 0000000000000000000000000000000000000000..1892f81e87b3ff09aeefeddc274fd81444689cdd
--- /dev/null
+++ b/lib/sys/prometheus-metrics.js
@@ -0,0 +1,43 @@
+'use strict'
+
+const prometheus = require('prom-client')
+
+class PrometheusMetrics {
+  constructor(config = {}) {
+    this.enabled = config.enabled || false
+    const matchNothing = /(?!)/
+    this.allowedIps = config.allowedIps
+      ? new RegExp(config.allowedIps)
+      : matchNothing
+    if (this.enabled) {
+      console.log(
+        `Metrics are enabled. Access to /metrics resoure is limited to IP addresses matching: ${
+          this.allowedIps
+        }`
+      )
+    }
+  }
+
+  async initialize(server) {
+    if (this.enabled) {
+      const register = prometheus.register
+      prometheus.collectDefaultMetrics()
+      this.setRoutes(server, register)
+    }
+  }
+
+  setRoutes(server, register) {
+    server.route(/^\/metrics$/, (data, match, end, ask) => {
+      const ip = ask.req.socket.remoteAddress
+      if (this.allowedIps.test(ip)) {
+        ask.res.setHeader('Content-Type', register.contentType)
+        ask.res.end(register.metrics())
+      } else {
+        ask.res.statusCode = 403
+        ask.res.end()
+      }
+    })
+  }
+}
+
+module.exports = PrometheusMetrics
diff --git a/lib/sys/prometheus-metrics.spec.js b/lib/sys/prometheus-metrics.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..32d1861e77567fa16108c9defe789cc066679164
--- /dev/null
+++ b/lib/sys/prometheus-metrics.spec.js
@@ -0,0 +1,93 @@
+'use strict'
+
+const { expect } = require('chai')
+const Camp = require('camp')
+const fetch = require('node-fetch')
+const config = require('../test-config')
+const Metrics = require('./prometheus-metrics')
+
+describe('Prometheus metrics route', function() {
+  const baseUrl = `http://127.0.0.1:${config.port}`
+
+  let camp
+  afterEach(function(done) {
+    if (camp) {
+      camp.close(() => done())
+      camp = undefined
+    }
+  })
+
+  function startServer(metricsConfig) {
+    return new Promise((resolve, reject) => {
+      camp = Camp.start({ port: config.port, hostname: '::' })
+      const metrics = new Metrics(metricsConfig)
+      metrics.initialize(camp)
+      camp.on('listening', () => resolve())
+    })
+  }
+
+  it('returns 404 when metrics are disabled', async function() {
+    startServer({ enabled: false })
+
+    const res = await fetch(`${baseUrl}/metrics`)
+
+    expect(res.status).to.be.equal(404)
+    expect(await res.text()).to.not.contains('nodejs_version_info')
+  })
+
+  it('returns 404 when there is no configuration', async function() {
+    startServer()
+
+    const res = await fetch(`${baseUrl}/metrics`)
+
+    expect(res.status).to.be.equal(404)
+    expect(await res.text()).to.not.contains('nodejs_version_info')
+  })
+
+  it('returns metrics for allowed IP', async function() {
+    startServer({
+      enabled: true,
+      allowedIps: '^(127\\.0\\.0\\.1|::1|::ffff:127\\.0\\.0\\.1)$',
+    })
+
+    const res = await fetch(`${baseUrl}/metrics`)
+
+    expect(res.status).to.be.equal(200)
+    expect(await res.text()).to.contains('nodejs_version_info')
+  })
+
+  it('returns metrics for request from allowed remote address', async function() {
+    startServer({
+      enabled: true,
+      allowedIps: '^(127\\.0\\.0\\.1|::1|::ffff:127\\.0\\.0\\.1)$',
+    })
+
+    const res = await fetch(`${baseUrl}/metrics`)
+
+    expect(res.status).to.be.equal(200)
+    expect(await res.text()).to.contains('nodejs_version_info')
+  })
+
+  it('returns 403 for not allowed IP', async function() {
+    startServer({
+      enabled: true,
+      allowedIps: '^127\\.0\\.0\\.200$',
+    })
+
+    const res = await fetch(`${baseUrl}/metrics`)
+
+    expect(res.status).to.be.equal(403)
+    expect(await res.text()).to.not.contains('nodejs_version_info')
+  })
+
+  it('returns 403 for every request when list with allowed IPs not defined', async function() {
+    startServer({
+      enabled: true,
+    })
+
+    const res = await fetch(`${baseUrl}/metrics`)
+
+    expect(res.status).to.be.equal(403)
+    expect(await res.text()).to.not.contains('nodejs_version_info')
+  })
+})
diff --git a/package-lock.json b/package-lock.json
index 6bb41e9f654e29d1909db5e423e75f500a851b73..6c819a6348b1de58d3195721e211d130b8a66c0e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1887,6 +1887,11 @@
       "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==",
       "dev": true
     },
+    "bintrees": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
+      "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
+    },
     "blob": {
       "version": "0.0.5",
       "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
@@ -12213,6 +12218,14 @@
       "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==",
       "dev": true
     },
+    "prom-client": {
+      "version": "11.1.2",
+      "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.1.2.tgz",
+      "integrity": "sha512-w06+FdkNw91QhzpJvw8eT6T2Od/7gfW5OCv3bmGutbb8/6Hu28f3qpoNSwCT/Al+KZABnzFaSNILcS/aazi2ew==",
+      "requires": {
+        "tdigest": "^0.1.1"
+      }
+    },
     "promise": {
       "version": "7.0.4",
       "resolved": "https://registry.npmjs.org/promise/-/promise-7.0.4.tgz",
@@ -14448,6 +14461,14 @@
       "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=",
       "dev": true
     },
+    "tdigest": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz",
+      "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=",
+      "requires": {
+        "bintrees": "1.0.1"
+      }
+    },
     "temp-write": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-2.1.0.tgz",
@@ -15110,7 +15131,7 @@
       "dependencies": {
         "chalk": {
           "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
           "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
           "dev": true,
           "requires": {
diff --git a/package.json b/package.json
index b865835d2bea4baba80813b09b7c4235aeab0332..98ec9bffaae1a1355a14c1e8b587badeb6f93098 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
     "pdfkit": "~0.8.0",
     "pretty-bytes": "^5.0.0",
     "priorityqueuejs": "^1.0.0",
+    "prom-client": "^11.1.2",
     "query-string": "^6.0.0",
     "raven": "^2.4.2",
     "redis": "~2.8.0",
diff --git a/server.js b/server.js
index e72cbebd8f1ad31e441ad61d3879e2ddd0dcbf96..7aac2a71b5dec95e7490f08fe98d21d2edde4173 100644
--- a/server.js
+++ b/server.js
@@ -16,6 +16,7 @@ const { checkErrorResponse } = require('./lib/error-helper')
 const analytics = require('./lib/analytics')
 const config = require('./lib/server-config')
 const GithubConstellation = require('./services/github/github-constellation')
+const PrometheusMetrics = require('./lib/sys/prometheus-metrics')
 const sysMonitor = require('./lib/sys/monitor')
 const log = require('./lib/log')
 const { makeMakeBadgeFn } = require('./lib/make-badge')
@@ -50,6 +51,7 @@ const githubConstellation = new GithubConstellation({
   persistence: config.persistence,
   service: config.services.github,
 })
+const metrics = new PrometheusMetrics(config.metrics.prometheus)
 const { apiProvider: githubApiProvider } = githubConstellation
 
 function reset() {
@@ -92,6 +94,7 @@ if (serverSecrets && serverSecrets.shieldsSecret) {
 }
 
 githubConstellation.initialize(camp)
+metrics.initialize(camp)
 
 suggest.setRoutes(config.cors.allowedOrigin, githubApiProvider, camp)