diff --git a/Gemfile b/Gemfile
index 6b85193690bacbb68562e5d03606132c4cf67a57..1341e45de802e380f5800d10d520b7cea6c9b9fa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -47,6 +47,7 @@ gem 'sidekiq'
 gem 'rails-settings-cached'
 gem 'pg_search'
 gem 'simple-navigation'
+gem 'statsd-instrument'
 
 gem 'react-rails'
 gem 'browserify-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2c009955eb6ba2b831d908255c036bae83edd83e..7214d21eb6f1e023a4735740a95ee257f9dd01cc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -370,6 +370,7 @@ GEM
       actionpack (>= 4.0)
       activesupport (>= 4.0)
       sprockets (>= 3.0.0)
+    statsd-instrument (2.1.2)
     temple (0.7.7)
     term-ansicolor (1.4.0)
       tins (~> 1.0)
@@ -463,6 +464,7 @@ DEPENDENCIES
   simple-navigation
   simple_form
   simplecov
+  statsd-instrument
   uglifier (>= 1.3.0)
   webmock
   will_paginate
diff --git a/app/lib/statsd_monitor.rb b/app/lib/statsd_monitor.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e48ce654106e1dc5a8cc296031459237a6759a63
--- /dev/null
+++ b/app/lib/statsd_monitor.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class StatsDMonitor
+  def initialize(app)
+    @app = app
+  end
+
+  def call(env)
+    @app.call(env)
+  end
+end
diff --git a/config/application.rb b/config/application.rb
index e561d04738e91a59baf0550872be55d7a2a3c23d..e97fb165ba8b92ed32a427fec4debc064c911cbf 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -30,6 +30,8 @@ module Mastodon
 
     config.active_job.queue_adapter = :sidekiq
 
+    config.middleware.insert(0, 'StatsDMonitor')
+
     config.middleware.insert_before 0, Rack::Cors do
       allow do
         origins  '*'
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 8b8d974b31571ae1200ecba6d52b57ac114c5e1a..30170e8105d9bdb202697ac9ced95772ad1558ff 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -104,4 +104,8 @@ Rails.application.configure do
   config.react.variant = :production
 
   config.active_record.logger = nil
+
+  config.to_prepare do
+    StatsD.backend = StatsD::Instrument::Backends::NullBackend if ENV['STATSD_ADDR'].blank?
+  end
 end
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index ac033bf9dc846101320c96a5ce8aceb8c96ec098..8fd1ae72c8fb7afa8e3417057ce19f9b288f1b9c 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -10,7 +10,6 @@
 #   inflect.uncountable %w( fish sheep )
 # end
 
-# These inflection rules are supported but not enabled by default:
-# ActiveSupport::Inflector.inflections(:en) do |inflect|
-#   inflect.acronym 'RESTful'
-# end
+ActiveSupport::Inflector.inflections(:en) do |inflect|
+  inflect.acronym 'StatsD'
+end
diff --git a/config/initializers/statsd.rb b/config/initializers/statsd.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c9c754e7ff7642dc3b91b35a61836b2bee545205
--- /dev/null
+++ b/config/initializers/statsd.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+StatsD.prefix              = 'mastodon'
+StatsD.default_sample_rate = 1
+
+StatsDMonitor.extend(StatsD::Instrument)
+StatsDMonitor.statsd_measure(:call, 'request.duration')
+
+STATSD_REQUEST_METRICS = {
+  'request.status.success'               => 200,
+  'request.status.not_found'             => 404,
+  'request.status.too_many_requests'     => 429,
+  'request.status.internal_server_error' => 500,
+}.freeze
+
+STATSD_REQUEST_METRICS.each do |name, code|
+  StatsDMonitor.statsd_count_if(:call, name) do |status, _env, _body|
+    status.to_i == code
+  end
+end