diff --git a/.gitignore b/.gitignore
index 963e1bfc7b75f8a2afb15e0accdde1e001b17af0..1c9a6b8c5cf4d97c289f7d7009457adab7175d6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ vendor/bundle/
 vendor/cache/
 config/database.yml
 config/oidc_key.pem
+config/schedule.yml
 
 # Generated files
 log/
diff --git a/Changelog.md b/Changelog.md
index 86eb81f1b73213056d34f27eb2c940c92edb29c9..64c2ed26e25c60cfaf1d6ef44c68cbd5df458cc7 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -3,6 +3,7 @@
 ## Refactor
 * Remove the 'make contacts in this aspect visible to each other' option [#7769](https://github.com/diaspora/diaspora/pull/7769)
 * Remove the requirement to have at least two users to disable the /podmin redirect [#7783](https://github.com/diaspora/diaspora/pull/7783)
+* Randomize start times of daily Sidekiq-Cron jobs [#7787](https://github.com/diaspora/diaspora/pull/7787)
 
 ## Bug fixes
 * Prefill conversation form on contacts page only with mutual contacts [#7744](https://github.com/diaspora/diaspora/pull/7744)
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 50e2e07da5701ffa0ab6fc2a3fdeefa0bcd1c04d..7b2a28a24b1cb94653f63a3c80291b0405ef5b22 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -54,9 +54,3 @@ end
 Sidekiq.configure_client do |config|
   config.redis = AppConfig.get_redis_options
 end
-
-schedule_file = "config/schedule.yml"
-
-if File.exist?(schedule_file) && Sidekiq.server?
-  Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
-end
diff --git a/config/initializers/sidekiq_scheduled.rb b/config/initializers/sidekiq_scheduled.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ac9140e380dfa9b3f34f8a17aeed9c156c30e723
--- /dev/null
+++ b/config/initializers/sidekiq_scheduled.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+# Some recurring background jobs can take a lot of resources, and others even
+# include pinging other pods, like recurring_pod_check. Having all jobs run at
+# 0 UTC causes a high local load, as well as a little bit of DDoSing through
+# the network, as pods try to ping each other.
+#
+# As Sidekiq-Cron does not support random offsets, we have to take care of that
+# ourselves, so let's add jobs with random work times.
+
+# rubocop:disable Metrics/MethodLength
+def default_job_config
+  random_hour = lambda { rand(24) }
+  random_minute = lambda { rand(60) }
+
+  {
+    check_birthday:          {
+      "cron":  "0 0 * * *",
+      "class": "Workers::CheckBirthday"
+    },
+
+    clean_cached_files:      {
+      "cron":  "#{random_minute.call} #{random_hour.call} * * *",
+      "class": "Workers::CleanCachedFiles"
+    },
+
+    cleanup_old_exports:     {
+      "cron":  "#{random_minute.call} #{random_hour.call} * * *",
+      "class": "Workers::CleanupOldExports"
+    },
+
+    queue_users_for_removal: {
+      "cron":  "#{random_minute.call} #{random_hour.call} * * *",
+      "class": "Workers::QueueUsersForRemoval"
+    },
+
+    recheck_scheduled_pods:  {
+      "cron":  "*/30 * * * *",
+      "class": "Workers::RecheckScheduledPods"
+    },
+
+    recurring_pod_check:     {
+      "cron":  "#{random_minute.call} #{random_hour.call} * * *",
+      "class": "Workers::RecurringPodCheck"
+    }
+  }
+end
+# rubocop:enable Metrics/MethodLength
+
+def valid_config?(path)
+  return false unless File.exist?(path)
+
+  current_config = YAML.load_file(path)
+
+  # If they key don't match the current default config keys, a new job has
+  # been added, so we nede to regenerate the config to have the new job
+  # running
+  return false unless current_config.keys == default_job_config.keys
+
+  # If recurring_pod_check is still running at midnight UTC, the config file
+  # is probably from a previous version, and that's bad, so we need to
+  # regenerate
+  current_config[:recurring_pod_check][:cron] != "0 0 * * *"
+end
+
+def regenerate_config(path)
+  job_config = default_job_config
+  File.open(path, "w") do |schedule_file|
+    schedule_file.write(job_config.to_yaml)
+  end
+end
+
+if Sidekiq.server?
+  schedule_file_path = Rails.root.join("config", "schedule.yml")
+  regenerate_config(schedule_file_path) unless valid_config?(schedule_file_path)
+
+  Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file_path)
+end
diff --git a/config/schedule.rb.example b/config/schedule.rb.example
deleted file mode 100644
index 319c9aedbe72ced865360e714aedaf1b6bc260ef..0000000000000000000000000000000000000000
--- a/config/schedule.rb.example
+++ /dev/null
@@ -1,25 +0,0 @@
-# Use this file to easily define all of your cron jobs.
-#
-# It's helpful, but not entirely necessary to understand cron before proceeding.
-# http://en.wikipedia.org/wiki/Cron
-
-# set :environment, "production"
-
-# Example:
-set :output, File.join( File.dirname( __FILE__ ), '..', 'logs', 'scheduled_tasks.log' )
-
-every 1.day, :at => '3:00 am' do
-  rake 'maintenance:clear_carrierwave_temp_uploads'
-end
-
-# every 2.hours do
-#   command "/usr/bin/some_great_command"
-#   runner "MyModel.some_method"
-#   rake "some:great:rake:task"
-# end
-#
-# every 4.days do
-#   runner "AnotherModel.prune_old_records"
-# end
-
-# Learn more: http://github.com/javan/whenever
diff --git a/config/schedule.yml b/config/schedule.yml
deleted file mode 100644
index 8e6f2361f830646549947115eb7b289007cc11b5..0000000000000000000000000000000000000000
--- a/config/schedule.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-clean_cached_files:
-  cron: "0 0 * * *"
-  class: "Workers::CleanCachedFiles"
-
-queue_users_for_removal:
-  cron: "0 0 * * *"
-  class: "Workers::QueueUsersForRemoval"
-
-recurring_pod_check:
-  cron: "0 0 * * *"
-  class: "Workers::RecurringPodCheck"
-
-recheck_scheduled_pods:
-  cron: "*/30 * * * *"
-  class: "Workers::RecheckScheduledPods"
-
-check_birthday:
-  cron: "0 0 * * *"
-  class: "Workers::CheckBirthday"
-
-cleanup_old_exports:
-  cron: "0 0 * * *"
-  class: "Workers::CleanupOldExports"