diff --git a/config/config.exs b/config/config.exs
index cc3e1044a707be3885687ded6b7e1d7e0991288f..5532692400807a59583fc0e75ff5e3d490ec844e 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -118,7 +118,9 @@ extra_cron = [
   # Daily at midday
   {"0 12 * * *", Plausible.Workers.SendCheckStatsEmails},
   # Every 10 minutes
-  {"*/10 * * * *", Plausible.Workers.ProvisionSslCertificates}
+  {"*/10 * * * *", Plausible.Workers.ProvisionSslCertificates},
+  # Every 15 minutes
+  {"*/15 * * * *", Plausible.Workers.SpikeNotifier}
 ]
 
 base_queues = [rotate_salts: 1]
@@ -130,7 +132,8 @@ extra_queues = [
   site_setup_emails: 1,
   trial_notification_emails: 1,
   schedule_email_reports: 1,
-  send_email_reports: 1
+  send_email_reports: 1,
+  spike_notifications: 1
 ]
 
 config :plausible, Oban,
diff --git a/config/releases.exs b/config/releases.exs
index a84b9306d45fa68f7952b4a07cbcce9e9785e6df..78fa2f57c2d9968605c2a0249c9f7852274ad3ae 100644
--- a/config/releases.exs
+++ b/config/releases.exs
@@ -157,7 +157,9 @@ extra_cron = [
   # Daily at midday
   {"0 12 * * *", Plausible.Workers.SendCheckStatsEmails},
   # Every 10 minutes
-  {"*/10 * * * *", Plausible.Workers.ProvisionSslCertificates}
+  {"*/10 * * * *", Plausible.Workers.ProvisionSslCertificates},
+  # Every 15 minutes
+  {"*/15 * * * *", Plausible.Workers.SpikeNotifier}
 ]
 
 base_queues = [rotate_salts: 1]
@@ -169,7 +171,8 @@ extra_queues = [
   site_setup_emails: 1,
   trial_notification_emails: 1,
   schedule_email_reports: 1,
-  send_email_reports: 1
+  send_email_reports: 1,
+  spike_notifications: 1
 ]
 
 config :plausible, Oban,
diff --git a/lib/plausible/site/schema.ex b/lib/plausible/site/schema.ex
index 8fe6582c74636f6ca507d1bb63835c5082e8a3e2..985296dce572bd3d2c54529a3f01f5136ecadd36 100644
--- a/lib/plausible/site/schema.ex
+++ b/lib/plausible/site/schema.ex
@@ -14,6 +14,7 @@ defmodule Plausible.Site do
     has_one :weekly_report, Plausible.Site.WeeklyReport
     has_one :monthly_report, Plausible.Site.MonthlyReport
     has_one :custom_domain, Plausible.Site.CustomDomain
+    has_one :spike_notification, Plausible.Site.SpikeNotification
 
     timestamps()
   end
diff --git a/lib/plausible/site/spike_notification.ex b/lib/plausible/site/spike_notification.ex
new file mode 100644
index 0000000000000000000000000000000000000000..9cca56cddf013276f922a37a61915ea26a829c34
--- /dev/null
+++ b/lib/plausible/site/spike_notification.ex
@@ -0,0 +1,30 @@
+defmodule Plausible.Site.SpikeNotification do
+  use Ecto.Schema
+  import Ecto.Changeset
+
+  schema "spike_notifications" do
+    field :recipients, {:array, :string}
+    field :threshold, :integer
+    field :last_sent, :naive_datetime
+    belongs_to :site, Plausible.Site
+
+    timestamps()
+  end
+
+  def changeset(settings, attrs \\ %{}) do
+    settings
+    |> cast(attrs, [:site_id, :recipients])
+    |> validate_required([:site_id, :recipients])
+    |> unique_constraint(:site)
+  end
+
+  def add_recipient(report, recipient) do
+    report
+    |> change(recipients: report.recipients ++ [recipient])
+  end
+
+  def remove_recipient(report, recipient) do
+    report
+    |> change(recipients: List.delete(report.recipients, recipient))
+  end
+end
diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex
index d6ac2a1c6dd0b8ad472529774f915c7952fa5c81..490d99658ee9ff69c3c96d5b2a506af7187c0032 100644
--- a/lib/plausible_web/email.ex
+++ b/lib/plausible_web/email.ex
@@ -13,7 +13,6 @@ defmodule PlausibleWeb.Email do
     |> subject("Activate your Plausible free trial")
     |> render("activation_email.html", name: user.name, link: link)
   end
-
   def welcome_email(user) do
     base_email()
     |> to(user)
@@ -94,6 +93,14 @@ defmodule PlausibleWeb.Email do
     |> render("weekly_report.html", Keyword.put(assigns, :site, site))
   end
 
+  def spike_notification(email, site, current_visitors) do
+    base_email()
+    |> to(email)
+    |> tag("spike-notification")
+    |> subject("Traffic spike on #{site.domain}")
+    |> render("spike_notification.html", %{current_visitors: current_visitors})
+  end
+
   def cancellation_email(user) do
     base_email()
     |> to(user.email)
diff --git a/lib/plausible_web/templates/email/spike_notification.html.eex b/lib/plausible_web/templates/email/spike_notification.html.eex
new file mode 100644
index 0000000000000000000000000000000000000000..906bb3a656e2a82c42456d40201d3d05ee25dafd
--- /dev/null
+++ b/lib/plausible_web/templates/email/spike_notification.html.eex
@@ -0,0 +1 @@
+Yours site has <%= @current_visitors %> visitors. Nice!
diff --git a/lib/workers/spike_notifier.ex b/lib/workers/spike_notifier.ex
new file mode 100644
index 0000000000000000000000000000000000000000..1faa9060ef95c3ca7f5d8f9bc5dc7ea032a6dced
--- /dev/null
+++ b/lib/workers/spike_notifier.ex
@@ -0,0 +1,39 @@
+defmodule Plausible.Workers.SpikeNotifier do
+  use Plausible.Repo
+  alias Plausible.Stats.Query
+  use Oban.Worker, queue: :spike_notifications
+  @at_most_every "12 hours"
+
+  @impl Oban.Worker
+  def perform(_args, _job, clickhouse \\ Plausible.Stats.Clickhouse) do
+    notifications = Repo.all(
+      from sn in Plausible.Site.SpikeNotification,
+      where: is_nil(sn.last_sent),
+      or_where: sn.last_sent < fragment("now() - INTERVAL ?", @at_most_every)
+    )
+
+    for notification <- notifications do
+      notification = Repo.preload(notification, :site)
+      query = Query.from(notification.site.timezone, %{"period" => "realtime"})
+      current_visitors = clickhouse.current_visitors(notification.site, query)
+      notify(notification, current_visitors)
+    end
+  end
+
+  def notify(notification, current_visitors) do
+    if current_visitors >= notification.threshold do
+      for recipient <- notification.recipients do
+        send_notification(recipient, notification.site, current_visitors)
+      end
+    end
+  end
+
+  defp send_notification(recipient, site, current_visitors) do
+    template = PlausibleWeb.Email.spike_notification(recipient, site, current_visitors)
+    try do
+      Plausible.Mailer.send_email(template)
+    rescue
+      _ -> nil
+    end
+  end
+end
diff --git a/mix.exs b/mix.exs
index 017c2982da3947d3442ecd9d38853e51239a66a9..146b1a4ccd4a2f88ce851028e45194526ec03eac 100644
--- a/mix.exs
+++ b/mix.exs
@@ -90,7 +90,8 @@ defmodule Plausible.MixProject do
       {:sshex, "2.2.1"},
       {:geolix, "~> 1.0"},
       {:clickhouse_ecto, git: "https://github.com/plausible/clickhouse_ecto.git"},
-      {:geolix_adapter_mmdb2, "~> 0.5.0"}
+      {:geolix_adapter_mmdb2, "~> 0.5.0"},
+      {:mix_test_watch, "~> 1.0", only: :dev}
     ]
   end
 
diff --git a/mix.lock b/mix.lock
index 90c62b087325ecd78b9bbc04650cdb1915a54ce2..b4a89c0b180aa5e5943316580c41a8b1fd43fd8a 100644
--- a/mix.lock
+++ b/mix.lock
@@ -44,6 +44,7 @@
   "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
   "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
   "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
+  "mix_test_watch": {:hex, :mix_test_watch, "1.0.2", "34900184cbbbc6b6ed616ed3a8ea9b791f9fd2088419352a6d3200525637f785", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "47ac558d8b06f684773972c6d04fcc15590abdb97aeb7666da19fcbfdc441a07"},
   "mmdb2_decoder": {:hex, :mmdb2_decoder, "3.0.0", "54828676a36e75e9a25bc9a0bb0598d4c7fcc767bf0b40674850b22e05b7b6cc", [:mix], [], "hexpm", "359dc9242915538d1dceb9f6d96c72201dca76ce62e49d22e2ed1e86f20bea8e"},
   "nanoid": {:hex, :nanoid, "2.0.2", "f3f7b4bf103ab6667f22beb00b6315825ee3f30100dd2c93d534e5c02164e857", [:mix], [], "hexpm", "3095cb1fac7bbc78843a8ccd99f1af375d0da1d3ebaa8552e846b73438c0c44f"},
   "oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"},
diff --git a/priv/repo/migrations/20201208173543_add_spike_notifications.exs b/priv/repo/migrations/20201208173543_add_spike_notifications.exs
new file mode 100644
index 0000000000000000000000000000000000000000..a5386470e468ff689072b4a7db55e166ded87f1e
--- /dev/null
+++ b/priv/repo/migrations/20201208173543_add_spike_notifications.exs
@@ -0,0 +1,14 @@
+defmodule Plausible.Repo.Migrations.AddSpikeNotifications do
+  use Ecto.Migration
+
+  def change do
+    create table(:spike_notifications) do
+      add :site_id, references(:sites), null: false
+      add :threshold, :integer, null: false
+      add :last_sent, :naive_datetime
+      add :recipients, {:array, :citext}, null: false, default: []
+
+      timestamps()
+    end
+  end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index ec7cde25fc53d96c2d3bfcc951b62cdbea8260e8..c0a341157e6b7ffd3980b145ca4da7728381cbed 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -20,6 +20,10 @@ defmodule Plausible.Factory do
     merge_attributes(user, attrs)
   end
 
+  def spike_notification_factory do
+    %Plausible.Site.SpikeNotification{}
+  end
+
   def site_factory do
     domain = sequence(:domain, &"example-#{&1}.com")
 
diff --git a/test/workers/spike_notifier_test.exs b/test/workers/spike_notifier_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..fb673cb62ab7bdecbbbd098cdd28266000a8b2b7
--- /dev/null
+++ b/test/workers/spike_notifier_test.exs
@@ -0,0 +1,44 @@
+defmodule Plausible.Workers.SpikeNotifierTest do
+  use Plausible.DataCase
+  use Bamboo.Test
+  import Double
+  alias Plausible.Workers.SpikeNotifier
+
+  test "does not notify anyone if current visitors does not exceed notification threshold" do
+    site = insert(:site)
+    insert(:spike_notification, site: site, threshold: 10, recipients: ["jerod@example.com", "uku@example.com"])
+
+    clickhouse_stub = stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site, _query -> 5 end)
+    SpikeNotifier.perform(nil, nil, clickhouse_stub)
+
+    assert_no_emails_delivered()
+  end
+
+  test "notifies all recipients when traffic is higher than configured threshold" do
+    site = insert(:site)
+    insert(:spike_notification, site: site, threshold: 10, recipients: ["jerod@example.com", "uku@example.com"])
+
+    clickhouse_stub = stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site, _query -> 10 end)
+    SpikeNotifier.perform(nil, nil, clickhouse_stub)
+
+    assert_email_delivered_with(
+      subject: "Traffic spike on #{site.domain}",
+      to: [nil: "jerod@example.com"]
+    )
+
+    assert_email_delivered_with(
+      subject: "Traffic spike on #{site.domain}",
+      to: [nil: "uku@example.com"]
+    )
+  end
+
+  test "does not notify anyone if a notification already went out in the last 12 hours" do
+    site = insert(:site)
+    insert(:spike_notification, site: site, threshold: 10, recipients: ["jerod@example.com", "uku@example.com"], last_sent: Timex.now())
+
+    clickhouse_stub = stub(Plausible.Stats.Clickhouse, :current_visitors, fn _site, _query -> 10 end)
+    SpikeNotifier.perform(nil, nil, clickhouse_stub)
+
+    assert_no_emails_delivered()
+  end
+end