diff --git a/CHANGELOG b/CHANGELOG
index 25b34ff2dcdc32a6d3ccafcac6151e3fa507f911..025fb3d6fed53d0a54e2c09685839ca48dccd53b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,7 @@ v 8.12.0 (unreleased)
   - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
   - Add ability to fork to a specific namespace using API. (ritave)
   - Cleanup misalignments in Issue list view !6206
+  - Prune events older than 12 months. (ritave)
   - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
   - Filter tags by name !6121
   - Make push events have equal vertical spacing.
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5883cafe1d16db9fdd29b4cd80e7c6dcc7868511
--- /dev/null
+++ b/app/workers/prune_old_events_worker.rb
@@ -0,0 +1,17 @@
+class PruneOldEventsWorker
+  include Sidekiq::Worker
+
+  def perform
+    # Contribution calendar shows maximum 12 months of events.
+    # Double nested query is used because MySQL doesn't allow DELETE subqueries
+    # on the same table.
+    Event.unscoped.where(
+      '(id IN (SELECT id FROM (?) ids_to_remove))',
+      Event.unscoped.where(
+        'created_at < ?',
+        (12.months + 1.day).ago).
+      select(:id).
+      limit(10_000)).
+    delete_all
+  end
+end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4a01b9e40fb3f4ca3b0fd39dc2a1bb9ca8c585d5..195108b921b749f8a85bc733450f0992153ce0b6 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -299,6 +299,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire
 Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
 Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
 Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
+Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
+Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
 
 #
 # GitLab Shell
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..35e1518a35e6c7fbb5f99c90659d34f45001cad5
--- /dev/null
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe PruneOldEventsWorker do
+  describe '#perform' do
+    let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) }
+    let!(:not_expired_event) { create(:event, author_id: 0,  created_at: 1.day.ago) }
+    let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) }
+
+    it 'prunes events older than 12 months' do
+      expect { subject.perform }.to change { Event.count }.by(-1)
+      expect(Event.find_by(id: expired_event.id)).to be_nil
+    end
+
+    it 'leaves fresh events' do
+      subject.perform
+      expect(not_expired_event.reload).to be_present
+    end
+
+    it 'leaves events from exactly 12 months ago' do
+      subject.perform
+      expect(exactly_12_months_event).to be_present
+    end
+  end
+end