diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb
index a0b7532c2e0ccedd25ba86504d635eea4ec519ea..961e82f21e24108c3c8c9f227bb5f2e01ad1ec5f 100644
--- a/app/controllers/activitypub/inboxes_controller.rb
+++ b/app/controllers/activitypub/inboxes_controller.rb
@@ -5,6 +5,7 @@ class ActivityPub::InboxesController < Api::BaseController
   include JsonLdHelper
 
   before_action :set_account
+  around_action :instrument_ingress
 
   def create
     if unknown_deleted_account?
@@ -51,4 +52,10 @@ class ActivityPub::InboxesController < Api::BaseController
   def process_payload
     ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body, @account&.id)
   end
+
+  def instrument_ingress
+    ActiveSupport::Notifications.instrument('activitypub.ingress', domain: signed_request_account.domain, ip: request.remote_ip) do
+      yield
+    end
+  end
 end
diff --git a/app/controllers/api/push_controller.rb b/app/controllers/api/push_controller.rb
deleted file mode 100644
index e04d19125b8d3ef67fd614e16216c5ee25539db3..0000000000000000000000000000000000000000
--- a/app/controllers/api/push_controller.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-class Api::PushController < Api::BaseController
-  include SignatureVerification
-
-  def update
-    response, status = process_push_request
-    render plain: response, status: status
-  end
-
-  private
-
-  def process_push_request
-    case hub_mode
-    when 'subscribe'
-      Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
-    when 'unsubscribe'
-      Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
-    else
-      ["Unknown mode: #{hub_mode}", 422]
-    end
-  end
-
-  def hub_mode
-    params['hub.mode']
-  end
-
-  def hub_topic
-    params['hub.topic']
-  end
-
-  def hub_callback
-    params['hub.callback']
-  end
-
-  def hub_lease_seconds
-    params['hub.lease_seconds']
-  end
-
-  def hub_secret
-    params['hub.secret']
-  end
-
-  def account_from_topic
-    if hub_topic.present? && local_domain? && account_feed_path?
-      Account.find_local(hub_topic_params[:username])
-    end
-  end
-
-  def hub_topic_params
-    @_hub_topic_params ||= Rails.application.routes.recognize_path(hub_topic_uri.path)
-  end
-
-  def hub_topic_uri
-    @_hub_topic_uri ||= Addressable::URI.parse(hub_topic).normalize
-  end
-
-  def local_domain?
-    TagManager.instance.web_domain?(hub_topic_domain)
-  end
-
-  def verified_domain
-    return signed_request_account.domain if signed_request_account
-  end
-
-  def hub_topic_domain
-    hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
-  end
-
-  def account_feed_path?
-    hub_topic_params[:controller] == 'accounts' && hub_topic_params[:action] == 'show' && hub_topic_params[:format] == 'atom'
-  end
-end
diff --git a/app/controllers/api/salmon_controller.rb b/app/controllers/api/salmon_controller.rb
deleted file mode 100644
index ac5f3268d8c78a28428369c0166f430d71515958..0000000000000000000000000000000000000000
--- a/app/controllers/api/salmon_controller.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-class Api::SalmonController < Api::BaseController
-  include SignatureVerification
-
-  before_action :set_account
-  respond_to :txt
-
-  def update
-    if verify_payload?
-      process_salmon
-      head 202
-    elsif payload.present?
-      render plain: signature_verification_failure_reason, status: 401
-    else
-      head 400
-    end
-  end
-
-  private
-
-  def set_account
-    @account = Account.find(params[:id])
-  end
-
-  def payload
-    @_payload ||= request.body.read
-  end
-
-  def verify_payload?
-    payload.present? && VerifySalmonService.new.call(payload)
-  end
-
-  def process_salmon
-    SalmonWorker.perform_async(@account.id, payload.force_encoding('UTF-8'))
-  end
-end
diff --git a/app/controllers/api/subscriptions_controller.rb b/app/controllers/api/subscriptions_controller.rb
deleted file mode 100644
index 89007f3d6efbe6f26c26bb68d66a11f7e576e48d..0000000000000000000000000000000000000000
--- a/app/controllers/api/subscriptions_controller.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-class Api::SubscriptionsController < Api::BaseController
-  before_action :set_account
-  respond_to :txt
-
-  def show
-    if subscription.valid?(params['hub.topic'])
-      @account.update(subscription_expires_at: future_expires)
-      render plain: encoded_challenge, status: 200
-    else
-      head 404
-    end
-  end
-
-  def update
-    if subscription.verify(body, request.headers['HTTP_X_HUB_SIGNATURE'])
-      ProcessingWorker.perform_async(@account.id, body.force_encoding('UTF-8'))
-    end
-
-    head 200
-  end
-
-  private
-
-  def subscription
-    @_subscription ||= @account.subscription(
-      api_subscription_url(@account.id)
-    )
-  end
-
-  def body
-    @_body ||= request.body.read
-  end
-
-  def encoded_challenge
-    HTMLEntities.new.encode(params['hub.challenge'])
-  end
-
-  def future_expires
-    Time.now.utc + lease_seconds_or_default
-  end
-
-  def lease_seconds_or_default
-    (params['hub.lease_seconds'] || 1.day).to_i.seconds
-  end
-
-  def set_account
-    @account = Account.find(params[:id])
-  end
-end
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 9a05d96cf9cf721fc4402f227db4c7be050e66ed..f5c0e85caef6428b4e52a060f42fd22c7a3b4335 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -53,8 +53,6 @@ class OStatus::AtomSerializer
     append_element(feed, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account))
     append_element(feed, 'link', nil, rel: :self, type: 'application/atom+xml', href: account_url(account, format: 'atom'))
     append_element(feed, 'link', nil, rel: :next, type: 'application/atom+xml', href: account_url(account, format: 'atom', max_id: stream_entries.last.id)) if stream_entries.size == 20
-    append_element(feed, 'link', nil, rel: :hub, href: api_push_url)
-    append_element(feed, 'link', nil, rel: :salmon, href: api_salmon_url(account.id))
 
     stream_entries.each do |stream_entry|
       feed << entry(stream_entry)
diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb
index 8c0b077020dcd2ce7833cf7063c8db3bf764bfb0..4220f697e6db66796e80eef2b984277c1c52dc64 100644
--- a/app/serializers/webfinger_serializer.rb
+++ b/app/serializers/webfinger_serializer.rb
@@ -18,7 +18,6 @@ class WebfingerSerializer < ActiveModel::Serializer
       { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
       { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
       { rel: 'self', type: 'application/activity+json', href: account_url(object) },
-      { rel: 'salmon', href: api_salmon_url(object.id) },
       { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" },
       { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
     ]
diff --git a/app/services/subscribe_service.rb b/app/services/subscribe_service.rb
index 83fd64396a1057d981ed67ca6f7c2e506ca78aac..c441e8598f9c871838d20a1f89568c11ecfe510c 100644
--- a/app/services/subscribe_service.rb
+++ b/app/services/subscribe_service.rb
@@ -2,57 +2,5 @@
 
 class SubscribeService < BaseService
   def call(account)
-    return if account.hub_url.blank?
-
-    @account        = account
-    @account.secret = SecureRandom.hex
-
-    build_request.perform do |response|
-      if response_failed_permanently? response
-        # We're not allowed to subscribe. Fail and move on.
-        @account.secret = ''
-        @account.save!
-      elsif response_successful? response
-        # The subscription will be confirmed asynchronously.
-        @account.save!
-      else
-        # The response was either a 429 rate limit, or a 5xx error.
-        # We need to retry at a later time. Fail loudly!
-        raise Mastodon::UnexpectedResponseError, response
-      end
-    end
-  end
-
-  private
-
-  def build_request
-    request = Request.new(:post, @account.hub_url, form: subscription_params)
-    request.on_behalf_of(some_local_account) if some_local_account
-    request
-  end
-
-  def subscription_params
-    {
-      'hub.topic': @account.remote_url,
-      'hub.mode': 'subscribe',
-      'hub.callback': api_subscription_url(@account.id),
-      'hub.verify': 'async',
-      'hub.secret': @account.secret,
-      'hub.lease_seconds': 7.days.seconds,
-    }
-  end
-
-  def some_local_account
-    @some_local_account ||= Account.local.without_suspended.first
-  end
-
-  # Any response in the 3xx or 4xx range, except for 429 (rate limit)
-  def response_failed_permanently?(response)
-    (response.status.redirect? || response.status.client_error?) && !response.status.too_many_requests?
-  end
-
-  # Any response in the 2xx range
-  def response_successful?(response)
-    response.status.success?
   end
 end
diff --git a/app/services/unsubscribe_service.rb b/app/services/unsubscribe_service.rb
index 95c1fb4fc01acf28e241d1cd6903734d880880c3..5fc40e63c6d48175b4cfd919029f59f81b3bedd2 100644
--- a/app/services/unsubscribe_service.rb
+++ b/app/services/unsubscribe_service.rb
@@ -2,35 +2,5 @@
 
 class UnsubscribeService < BaseService
   def call(account)
-    return if account.hub_url.blank?
-
-    @account = account
-
-    begin
-      build_request.perform do |response|
-        Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{response.status}" unless response.status.success?
-      end
-    rescue HTTP::Error, OpenSSL::SSL::SSLError => e
-      Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}"
-    end
-
-    @account.secret = ''
-    @account.subscription_expires_at = nil
-    @account.save!
-  end
-
-  private
-
-  def build_request
-    Request.new(:post, @account.hub_url, form: subscription_params)
-  end
-
-  def subscription_params
-    {
-      'hub.topic': @account.remote_url,
-      'hub.mode': 'unsubscribe',
-      'hub.callback': api_subscription_url(@account.id),
-      'hub.verify': 'async',
-    }
   end
 end
diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml
index 950e618477b8b006456da96f472ae2cbb501e9f3..de7d2a8ba3004ad8c17cf0efc2c40dd519ac6abc 100644
--- a/app/views/accounts/show.html.haml
+++ b/app/views/accounts/show.html.haml
@@ -7,7 +7,6 @@
   - if @account.user&.setting_noindex
     %meta{ name: 'robots', content: 'noindex' }/
 
-  %link{ rel: 'salmon', href: api_salmon_url(@account.id) }/
   %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
   %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
   %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby
index 968c8c1380c0d70543e60ce8f9348e18a5f1735d..c82cdb7b3d017420ac32a110e5e349296fc20e3b 100644
--- a/app/views/well_known/webfinger/show.xml.ruby
+++ b/app/views/well_known/webfinger/show.xml.ruby
@@ -25,11 +25,6 @@ doc << Ox::Element.new('XRD').tap do |xrd|
     link['href']     = account_url(@account)
   end
 
-  xrd << Ox::Element.new('Link').tap do |link|
-    link['rel']      = 'salmon'
-    link['href']     = api_salmon_url(@account.id)
-  end
-
   xrd << Ox::Element.new('Link').tap do |link|
     link['rel']      = 'magic-public-key'
     link['href']     = "data:application/magic-public-key,#{@account.magic_key}"
diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb
index 5e4c391f0d79977ad2d9fb0887f2515345ef0692..bc081f7b96dd4ee323c9b1378630119ceaebd44c 100644
--- a/app/workers/activitypub/delivery_worker.rb
+++ b/app/workers/activitypub/delivery_worker.rb
@@ -18,7 +18,9 @@ class ActivityPub::DeliveryWorker
     @source_account = Account.find(source_account_id)
     @inbox_url      = inbox_url
 
-    perform_request
+    ActiveSupport::Notifications.instrument('activitypub.egress', domain: Addressable::URI.parse(@inbox_url).normalized_host) do
+      perform_request
+    end
 
     failure_tracker.track_success!
   rescue => e
diff --git a/app/workers/pubsubhubbub/delivery_worker.rb b/app/workers/pubsubhubbub/delivery_worker.rb
index 619bfa48aad215c36f357114e608c62d8cbee336..077a8d676100ce43d7c54186a9cd3ede084dbf5a 100644
--- a/app/workers/pubsubhubbub/delivery_worker.rb
+++ b/app/workers/pubsubhubbub/delivery_worker.rb
@@ -52,11 +52,7 @@ class Pubsubhubbub::DeliveryWorker
   end
 
   def link_header
-    LinkHeader.new([hub_link_header, self_link_header]).to_s
-  end
-
-  def hub_link_header
-    [api_push_url, [%w(rel hub)]]
+    LinkHeader.new([self_link_header]).to_s
   end
 
   def self_link_header
diff --git a/config/initializers/instrumentation.rb b/config/initializers/instrumentation.rb
index 8483f2be2e4caf502fe10dc12298eb32b514442d..476ddc7a015e294a3235f721de9294af92865eef 100644
--- a/config/initializers/instrumentation.rb
+++ b/config/initializers/instrumentation.rb
@@ -16,3 +16,39 @@ ActiveSupport::Notifications.subscribe(/process_action.action_controller/) do |*
   ActiveSupport::Notifications.instrument :performance, action: :measure, measurement: "#{key}.view_time", value: event.payload[:view_runtime]
   ActiveSupport::Notifications.instrument :performance, measurement: "#{key}.status.#{status}"
 end
+
+EXPIRE_AFTER = 2.days.seconds.freeze
+
+ActiveSupport::Notifications.subscribe(/activitypub.(ingress|egress)/) do |*args|
+  event   = ActiveSupport::Notifications::Event.new(*args)
+  buckets = [event.started.to_i % 3_600, event.started.to_i % 86_400]
+
+  case event.name
+  when 'activitypub.ingress'
+    buckets.each do |bucket|
+      Redis.current.hincrby("counters:activitypub.ingress:#{bucket}", "domain:#{event.payload[:domain]}", 1)
+      Redis.current.hincrby("counters:activitypub.ingress:#{bucket}", "ip:#{event.payload[:ip]}", 1)
+      Redis.current.expire("counters:activitypub.ingress:#{bucket}", EXPIRE_AFTER)
+    end
+  when 'activitypub.egress'
+    buckets.each do |bucket|
+      Redis.current.hincrby("counters:activitypub.egress:#{bucket}", "domain:#{event.payload[:domain]}", 1)
+      Redis.current.expire("counters:activitypub.egress:#{bucket}", EXPIRE_AFTER)
+    end
+  end
+end
+
+def anomalies
+  now = Time.now.to_i
+
+  [3_600, 86_400].each do |interval|
+    current_period = Redis.current.hgetall("counters:activitypub.ingress:#{now % interval}")
+    past_period    = Redis.current.hgetall("counters:activitypub.ingress:#{now % interval - interval}")
+
+    current_period.each_pair do |key, value|
+      if value > past_period[key]
+        # Sound the alarm!
+      end
+    end
+  end
+end
diff --git a/config/navigation.rb b/config/navigation.rb
index df10241892997bfde20f25ac999d5e8ac7b8ae0a..ef845d1fc5957a5bd03e6fb6eb40e87a9f620c2f 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -48,7 +48,6 @@ SimpleNavigation::Configuration.run do |navigation|
       s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings}
       s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
       s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays}
-      s.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? }
       s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
       s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
     end
diff --git a/config/routes.rb b/config/routes.rb
index 764db8db2e61882d1dc2507de5a50fa04beb2f58..e6bc57a3158a1b2c63c3fb8c5b1c115452df7cad 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -154,7 +154,6 @@ Rails.application.routes.draw do
   namespace :admin do
     get '/dashboard', to: 'dashboard#index'
 
-    resources :subscriptions, only: [:index]
     resources :domain_blocks, only: [:new, :create, :show, :destroy]
     resources :email_domain_blocks, only: [:index, :new, :create, :destroy]
     resources :action_logs, only: [:index]
@@ -257,16 +256,6 @@ Rails.application.routes.draw do
   get '/admin', to: redirect('/admin/dashboard', status: 302)
 
   namespace :api do
-    # PubSubHubbub outgoing subscriptions
-    resources :subscriptions, only: [:show]
-    post '/subscriptions/:id', to: 'subscriptions#update'
-
-    # PubSubHubbub incoming subscriptions
-    post '/push', to: 'push#update', as: :push
-
-    # Salmon
-    post '/salmon/:id', to: 'salmon#update', as: :salmon
-
     # OEmbed
     get '/oembed', to: 'oembed#show', as: :oembed