diff --git a/Documentation/git-maintenance.txt b/Documentation/git-maintenance.txt
index 80ddd33ceb..f012923333 100644
--- a/Documentation/git-maintenance.txt
+++ b/Documentation/git-maintenance.txt
@@ -181,6 +181,20 @@ OPTIONS
 	`maintenance.<task>.enabled` configured as `true` are considered.
 	See the 'TASKS' section for the list of accepted `<task>` values.
 
+--scheduler=auto|crontab|systemd-timer|launchctl|schtasks::
+	When combined with the `start` subcommand, specify the scheduler
+	to use to run the hourly, daily and weekly executions of
+	`git maintenance run`.
+	The possible values for `<scheduler>` depend on the system: `crontab`
+	is available on POSIX systems, `systemd-timer` is available on Linux
+	systems, `launchctl` is available on MacOS and `schtasks` is available
+	on Windows.
+	By default or when `auto` is specified, the most appropriate scheduler
+	for the system is used. On MacOS, `launchctl` is used. On Windows,
+	`schtasks` is used. On Linux, `systemd-timers` is used if user systemd
+	timers are available, otherwise, `crontab` is used. On all other systems,
+	`crontab` is used.
+
 
 TROUBLESHOOTING
 ---------------
@@ -279,6 +293,52 @@ schedule to ensure you are executing the correct binaries in your
 schedule.
 
 
+BACKGROUND MAINTENANCE ON LINUX SYSTEMD SYSTEMS
+-----------------------------------------------
+
+While Linux supports `cron`, depending on the distribution, `cron` may
+be an optional package not necessarily installed. On modern Linux
+distributions, systemd timers are superseding it.
+
+If user systemd timers are available, they will be used as a replacement
+of `cron`.
+
+In this case, `git maintenance start` will create user systemd timer units
+and start the timers. The current list of user-scheduled tasks can be found
+by running `systemctl --user list-timers`. The timers written by `git
+maintenance start` are similar to this:
+
+-----------------------------------------------------------------------
+$ systemctl --user list-timers
+NEXT                         LEFT          LAST                         PASSED     UNIT                         ACTIVATES
+Thu 2021-04-29 19:00:00 CEST 42min left    Thu 2021-04-29 18:00:11 CEST 17min ago  git-maintenance@hourly.timer git-maintenance@hourly.service
+Fri 2021-04-30 00:00:00 CEST 5h 42min left Thu 2021-04-29 00:00:11 CEST 18h ago    git-maintenance@daily.timer  git-maintenance@daily.service
+Mon 2021-05-03 00:00:00 CEST 3 days left   Mon 2021-04-26 00:00:11 CEST 3 days ago git-maintenance@weekly.timer git-maintenance@weekly.service
+-----------------------------------------------------------------------
+
+One timer is registered for each `--schedule=<frequency>` option.
+
+The definition of the systemd units can be inspected in the following files:
+
+-----------------------------------------------------------------------
+~/.config/systemd/user/git-maintenance@.timer
+~/.config/systemd/user/git-maintenance@.service
+~/.config/systemd/user/timers.target.wants/git-maintenance@hourly.timer
+~/.config/systemd/user/timers.target.wants/git-maintenance@daily.timer
+~/.config/systemd/user/timers.target.wants/git-maintenance@weekly.timer
+-----------------------------------------------------------------------
+
+`git maintenance start` will overwrite these files and start the timer
+again with `systemctl --user`, so any customization should be done by
+creating a drop-in file, i.e. a `.conf` suffixed file in the
+`~/.config/systemd/user/git-maintenance@.service.d` directory.
+
+`git maintenance stop` will stop the user systemd timers and delete
+the above mentioned files.
+
+For more details, see systemd.timer(5)
+
+
 BACKGROUND MAINTENANCE ON MACOS SYSTEMS
 ---------------------------------------
 
diff --git a/builtin/gc.c b/builtin/gc.c
index ef7226d7bc..7c72aa3b99 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1544,6 +1544,15 @@ static const char *get_frequency(enum schedule_priority schedule)
 	}
 }
 
+static int is_launchctl_available(const char *cmd)
+{
+#ifdef __APPLE__
+	return 1;
+#else
+	return 0;
+#endif
+}
+
 static char *launchctl_service_name(const char *frequency)
 {
 	struct strbuf label = STRBUF_INIT;
@@ -1710,6 +1719,15 @@ static int launchctl_update_schedule(int run_maintenance, int fd, const char *cm
 		return launchctl_remove_plists(cmd);
 }
 
+static int is_schtasks_available(const char *cmd)
+{
+#ifdef GIT_WINDOWS_NATIVE
+	return 1;
+#else
+	return 0;
+#endif
+}
+
 static char *schtasks_task_name(const char *frequency)
 {
 	struct strbuf label = STRBUF_INIT;
@@ -1872,6 +1890,28 @@ static int schtasks_update_schedule(int run_maintenance, int fd, const char *cmd
 		return schtasks_remove_tasks(cmd);
 }
 
+static int is_crontab_available(const char *cmd)
+{
+	static int cached_result = -1;
+	struct child_process child = CHILD_PROCESS_INIT;
+
+	if (cached_result != -1)
+		return cached_result;
+
+	strvec_split(&child.args, cmd);
+	strvec_push(&child.args, "-l");
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.no_stderr = 1;
+	child.silent_exec_failure = 1;
+
+	if (start_command(&child))
+		return cached_result = 0;
+	/* Ignore exit code, as an empty crontab will return error. */
+	finish_command(&child);
+	return cached_result = 1;
+}
+
 #define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE"
 #define END_LINE "# END GIT MAINTENANCE SCHEDULE"
 
@@ -1959,61 +1999,348 @@ static int crontab_update_schedule(int run_maintenance, int fd, const char *cmd)
 	return result;
 }
 
+static int is_systemd_timer_available(const char *cmd)
+{
+#ifdef __linux__
+	static int cached_result = -1;
+	struct child_process child = CHILD_PROCESS_INIT;
+
+	if (cached_result != -1)
+		return cached_result;
+
+	strvec_split(&child.args, cmd);
+	strvec_pushl(&child.args, "--user", "list-timers", NULL);
+	child.no_stdin = 1;
+	child.no_stdout = 1;
+	child.no_stderr = 1;
+	child.silent_exec_failure = 1;
+
+	if (start_command(&child))
+		return cached_result = 0;
+	if (finish_command(&child))
+		return cached_result = 0;
+	return cached_result = 1;
+#else
+	return 0;
+#endif
+}
+
+static char *xdg_config_home_systemd(const char *filename)
+{
+	const char *home, *config_home;
+
+	assert(filename);
+	config_home = getenv("XDG_CONFIG_HOME");
+	if (config_home && *config_home)
+		return mkpathdup("%s/systemd/user/%s", config_home, filename);
+
+	home = getenv("HOME");
+	if (home)
+		return mkpathdup("%s/.config/systemd/user/%s", home, filename);
+
+	die(_("failed to get $XDG_CONFIG_HOME and $HOME"));
+}
+
+static int systemd_timer_enable_unit(int enable,
+				     enum schedule_priority schedule,
+				     const char *cmd)
+{
+	struct child_process child = CHILD_PROCESS_INIT;
+	const char *frequency = get_frequency(schedule);
+
+	strvec_split(&child.args, cmd);
+	strvec_pushl(&child.args, "--user", enable ? "enable" : "disable",
+		     "--now", NULL);
+	strvec_pushf(&child.args, "git-maintenance@%s.timer", frequency);
+
+	if (start_command(&child))
+		die(_("failed to run systemctl"));
+	return finish_command(&child);
+}
+
+static int systemd_timer_delete_unit_templates(void)
+{
+	char *filename = xdg_config_home_systemd("git-maintenance@.timer");
+	unlink(filename);
+	free(filename);
+
+	filename = xdg_config_home_systemd("git-maintenance@.service");
+	unlink(filename);
+	free(filename);
+
+	return 0;
+}
+
+static int systemd_timer_delete_units(const char *cmd)
+{
+	return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, cmd) ||
+	       systemd_timer_enable_unit(0, SCHEDULE_DAILY, cmd) ||
+	       systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, cmd) ||
+	       systemd_timer_delete_unit_templates();
+}
+
+static int systemd_timer_write_unit_templates(const char *exec_path)
+{
+	char *filename;
+	FILE *file;
+	const char *unit;
+
+	filename = xdg_config_home_systemd("git-maintenance@.timer");
+	if (safe_create_leading_directories(filename))
+		die(_("failed to create directories for '%s'"), filename);
+	file = xfopen(filename, "w");
+	free(filename);
+
+	unit = "# This file was created and is maintained by Git.\n"
+	       "# Any edits made in this file might be replaced in the future\n"
+	       "# by a Git command.\n"
+	       "\n"
+	       "[Unit]\n"
+	       "Description=Optimize Git repositories data\n"
+	       "\n"
+	       "[Timer]\n"
+	       "OnCalendar=%i\n"
+	       "Persistent=true\n"
+	       "\n"
+	       "[Install]\n"
+	       "WantedBy=timers.target\n";
+	fputs(unit, file);
+	fclose(file);
+
+	filename = xdg_config_home_systemd("git-maintenance@.service");
+	if (safe_create_leading_directories(filename))
+		die(_("failed to create directories for '%s'"), filename);
+	file = xfopen(filename, "w");
+	free(filename);
+
+	unit = "# This file was created and is maintained by Git.\n"
+	       "# Any edits made in this file might be replaced in the future\n"
+	       "# by a Git command.\n"
+	       "\n"
+	       "[Unit]\n"
+	       "Description=Optimize Git repositories data\n"
+	       "\n"
+	       "[Service]\n"
+	       "Type=oneshot\n"
+	       "ExecStart=\"%1$s/git\" --exec-path=\"%1$s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n"
+	       "LockPersonality=yes\n"
+	       "MemoryDenyWriteExecute=yes\n"
+	       "NoNewPrivileges=yes\n"
+	       "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n"
+	       "RestrictNamespaces=yes\n"
+	       "RestrictRealtime=yes\n"
+	       "RestrictSUIDSGID=yes\n"
+	       "SystemCallArchitectures=native\n"
+	       "SystemCallFilter=@system-service\n";
+	fprintf(file, unit, exec_path);
+	fclose(file);
+
+	return 0;
+}
+
+static int systemd_timer_setup_units(const char *cmd)
+{
+	const char *exec_path = git_exec_path();
+
+	return systemd_timer_write_unit_templates(exec_path) ||
+	       systemd_timer_enable_unit(1, SCHEDULE_HOURLY, cmd) ||
+	       systemd_timer_enable_unit(1, SCHEDULE_DAILY, cmd) ||
+	       systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, cmd);
+}
+
+static int systemd_timer_update_schedule(int run_maintenance, int fd,
+					 const char *cmd)
+{
+	if (run_maintenance)
+		return systemd_timer_setup_units(cmd);
+	else
+		return systemd_timer_delete_units(cmd);
+}
+
+enum scheduler {
+	SCHEDULER_INVALID = -1,
+	SCHEDULER_AUTO = 0,
+	SCHEDULER_CRON = 1,
+	SCHEDULER_SYSTEMD = 2,
+	SCHEDULER_LAUNCHCTL = 3,
+	SCHEDULER_SCHTASKS = 4,
+};
+
+static const struct {
+	int (*is_available)(const char *cmd);
+	int (*update_schedule)(int run_maintenance, int fd, const char *cmd);
+	const char *cmd;
+} scheduler_fn[] = {
+	[SCHEDULER_CRON] = { is_crontab_available, crontab_update_schedule,
+			     "crontab" },
+	[SCHEDULER_SYSTEMD] = { is_systemd_timer_available,
+				systemd_timer_update_schedule, "systemctl" },
+	[SCHEDULER_LAUNCHCTL] = { is_launchctl_available,
+				  launchctl_update_schedule, "launchctl" },
+	[SCHEDULER_SCHTASKS] = { is_schtasks_available,
+				 schtasks_update_schedule, "schtasks" },
+};
+
+static enum scheduler parse_scheduler(const char *value)
+{
+	if (!value)
+		return SCHEDULER_INVALID;
+	else if (!strcasecmp(value, "auto"))
+		return SCHEDULER_AUTO;
+	else if (!strcasecmp(value, "cron") || !strcasecmp(value, "crontab"))
+		return SCHEDULER_CRON;
+	else if (!strcasecmp(value, "systemd") ||
+		 !strcasecmp(value, "systemd-timer"))
+		return SCHEDULER_SYSTEMD;
+	else if (!strcasecmp(value, "launchctl"))
+		return SCHEDULER_LAUNCHCTL;
+	else if (!strcasecmp(value, "schtasks"))
+		return SCHEDULER_SCHTASKS;
+	else
+		return SCHEDULER_INVALID;
+}
+
+static int maintenance_opt_scheduler(const struct option *opt, const char *arg,
+				     int unset)
+{
+	enum scheduler *scheduler = opt->value;
+
+	if (unset)
+		die(_("--no-scheduler is not allowed"));
+
+	*scheduler = parse_scheduler(arg);
+
+	if (*scheduler == SCHEDULER_INVALID)
+		die(_("unrecognized --scheduler argument '%s'"), arg);
+
+	return 0;
+}
+
+struct maintenance_start_opts {
+	enum scheduler scheduler;
+};
+
+static void resolve_auto_scheduler(enum scheduler *scheduler)
+{
+	if (*scheduler != SCHEDULER_AUTO)
+		return;
+
 #if defined(__APPLE__)
-static const char platform_scheduler[] = "launchctl";
+	*scheduler = SCHEDULER_LAUNCHCTL;
+	return;
+
 #elif defined(GIT_WINDOWS_NATIVE)
-static const char platform_scheduler[] = "schtasks";
+	*scheduler = SCHEDULER_SCHTASKS;
+	return;
+
+#elif defined(__linux__)
+	if (is_systemd_timer_available("systemctl"))
+		*scheduler = SCHEDULER_SYSTEMD;
+	else if (is_crontab_available("crontab"))
+		*scheduler = SCHEDULER_CRON;
+	else
+		die(_("neither systemd timers nor crontab are available"));
+	return;
+
 #else
-static const char platform_scheduler[] = "crontab";
+	*scheduler = SCHEDULER_CRON;
+	return;
 #endif
+}
 
-static int update_background_schedule(int enable)
+static void validate_scheduler(enum scheduler scheduler)
 {
-	int result;
-	const char *scheduler = platform_scheduler;
-	const char *cmd = scheduler;
+	const char *cmd;
+
+	if (scheduler == SCHEDULER_INVALID)
+		BUG("invalid scheduler");
+	if (scheduler == SCHEDULER_AUTO)
+		BUG("resolve_auto_scheduler should have been called before");
+
+	cmd = scheduler_fn[scheduler].cmd;
+	if (!scheduler_fn[scheduler].is_available(cmd))
+		die(_("%s scheduler is not available"), cmd);
+}
+
+static int update_background_schedule(const struct maintenance_start_opts *opts,
+				      int enable)
+{
+	unsigned int i;
+	int res, result = 0;
+	enum scheduler scheduler;
+	const char *cmd = NULL;
 	char *testing;
 	struct lock_file lk;
 	char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path);
 
+	if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
+		return error(_("another process is scheduling background maintenance"));
+
 	testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER"));
 	if (testing) {
 		char *sep = strchr(testing, ':');
 		if (!sep)
 			die("GIT_TEST_MAINT_SCHEDULER unparseable: %s", testing);
 		*sep = '\0';
-		scheduler = testing;
+		scheduler = parse_scheduler(testing);
 		cmd = sep + 1;
+		result = scheduler_fn[scheduler].update_schedule(
+			enable, get_lock_file_fd(&lk), cmd);
+		goto done;
 	}
 
-	if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0)
-		return error(_("another process is scheduling background maintenance"));
-
-	if (!strcmp(scheduler, "launchctl"))
-		result = launchctl_update_schedule(enable, get_lock_file_fd(&lk), cmd);
-	else if (!strcmp(scheduler, "schtasks"))
-		result = schtasks_update_schedule(enable, get_lock_file_fd(&lk), cmd);
-	else if (!strcmp(scheduler, "crontab"))
-		result = crontab_update_schedule(enable, get_lock_file_fd(&lk), cmd);
-	else
-		die("unknown background scheduler: %s", scheduler);
+	for (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) {
+		int enable_scheduler = enable && (opts->scheduler == i);
+		cmd = scheduler_fn[i].cmd;
+		if (!scheduler_fn[i].is_available(cmd))
+			continue;
+		res = scheduler_fn[i].update_schedule(
+			enable_scheduler, get_lock_file_fd(&lk), cmd);
+		if (enable_scheduler)
+			result = res;
+	}
 
+done:
 	rollback_lock_file(&lk);
 	free(testing);
 	return result;
 }
 
-static int maintenance_start(void)
+static const char *const builtin_maintenance_start_usage[] = {
+	N_("git maintenance start [--scheduler=<scheduler>]"), NULL
+};
+
+static int maintenance_start(int argc, const char **argv, const char *prefix)
 {
+	struct maintenance_start_opts opts;
+	struct option builtin_maintenance_start_options[] = {
+		OPT_CALLBACK(
+			0, "scheduler", &opts.scheduler, N_("scheduler"),
+			N_("scheduler to use to trigger git maintenance run"),
+			maintenance_opt_scheduler),
+		OPT_END()
+	};
+	memset(&opts, 0, sizeof(opts));
+
+	argc = parse_options(argc, argv, prefix,
+			     builtin_maintenance_start_options,
+			     builtin_maintenance_start_usage, 0);
+
+	resolve_auto_scheduler(&opts.scheduler);
+	validate_scheduler(opts.scheduler);
+
+	if (argc > 0)
+		usage_with_options(builtin_maintenance_start_usage,
+				   builtin_maintenance_start_options);
+
 	if (maintenance_register())
 		warning(_("failed to add repo to global config"));
-
-	return update_background_schedule(1);
+	return update_background_schedule(&opts, 1);
 }
 
 static int maintenance_stop(void)
 {
-	return update_background_schedule(0);
+	return update_background_schedule(NULL, 0);
 }
 
 static const char builtin_maintenance_usage[] =	N_("git maintenance <subcommand> [<options>]");
@@ -2027,7 +2354,7 @@ int cmd_maintenance(int argc, const char **argv, const char *prefix)
 	if (!strcmp(argv[1], "run"))
 		return maintenance_run(argc - 1, argv + 1, prefix);
 	if (!strcmp(argv[1], "start"))
-		return maintenance_start();
+		return maintenance_start(argc - 1, argv + 1, prefix);
 	if (!strcmp(argv[1], "stop"))
 		return maintenance_stop();
 	if (!strcmp(argv[1], "register"))
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 2412d8c5c0..6e6316cd90 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -20,6 +20,20 @@ test_xmllint () {
 	fi
 }
 
+test_lazy_prereq SYSTEMD_ANALYZE '
+	systemd-analyze --help >out &&
+	grep verify out
+'
+
+test_systemd_analyze_verify () {
+	if test_have_prereq SYSTEMD_ANALYZE
+	then
+		systemd-analyze verify "$@"
+	else
+		true
+	fi
+}
+
 test_expect_success 'help text' '
 	test_expect_code 129 git maintenance -h 2>err &&
 	test_i18ngrep "usage: git maintenance <subcommand>" err &&
@@ -615,6 +629,43 @@ test_expect_success 'start and stop Windows maintenance' '
 	test_cmp expect args
 '
 
+test_expect_success 'start and stop Linux/systemd maintenance' '
+	write_script print-args <<-\EOF &&
+	echo $* >>args
+	EOF
+
+	rm -f args &&
+	GIT_TEST_MAINT_SCHEDULER="systemd:./print-args" git maintenance start &&
+
+	# start registers the repo
+	git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
+
+	test_systemd_analyze_verify "$HOME/.config/systemd/user/git-maintenance@.service" &&
+
+	rm -f expect &&
+	for frequency in hourly daily weekly
+	do
+		echo "--user enable --now git-maintenance@${frequency}.timer" >>expect || return 1
+	done &&
+	test_cmp expect args &&
+
+	rm -f args &&
+	GIT_TEST_MAINT_SCHEDULER="systemd:./print-args" git maintenance stop &&
+
+	# stop does not unregister the repo
+	git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
+
+	test_path_is_missing "$HOME/.config/systemd/user/git-maintenance@.timer" &&
+	test_path_is_missing "$HOME/.config/systemd/user/git-maintenance@.service" &&
+
+	rm -f expect &&
+	for frequency in hourly daily weekly
+	do
+		echo "--user disable --now git-maintenance@${frequency}.timer" >>expect || return 1
+	done &&
+	test_cmp expect args
+'
+
 test_expect_success 'register preserves existing strategy' '
 	git config maintenance.strategy none &&
 	git maintenance register &&
