Scheduling Automated Snapshots with SLM

Set an SLM policy to fire on a daily or hourly cadence by pairing a correct Elasticsearch cron expression with a date-math name template, so every snapshot is created at a predictable moment and carries a unique, chronologically-sortable name.

Scheduling is the one part of SLM where a small syntax mistake fails quietly rather than loudly. A malformed cron expression is usually rejected outright, which is easy to catch — but a well-formed expression that means something other than what you intended applies cleanly and then fires at the wrong time every day, and nobody notices until a restore reveals the backup window was never what the runbook claimed. This page focuses narrowly on getting the schedule and name fields right. It is one field-pair inside the larger job of configuring SLM policies, which in turn sits under Snapshot Lifecycle Management & Searchable Snapshots. The retention half of the policy — deciding how long those scheduled snapshots survive — is a separate concern covered in setting snapshot retention with SLM.

Prerequisites

  • Elasticsearch 8.x with the SLM service running (GET _slm/status reports RUNNING) and a registered repository named prod-s3-repo.
  • manage_slm cluster privilege on the account that creates or updates the policy.
  • A decided cadence — daily at a low-traffic hour, or hourly for higher-value data — and awareness that Elasticsearch interprets cron times in UTC unless the node timezone is otherwise configured.

The Elasticsearch cron format

Elasticsearch cron expressions have six fields, not the five of a Unix crontab, because a leading seconds field is prepended. The order is seconds minutes hours day-of-month month day-of-week. This single difference is responsible for the majority of mis-scheduled SLM policies: a five-field crontab pasted in shifts every value one position to the left, so 30 1 * * * (intended as 01:30) is read as second 30, minute 1, hour *, day 1 — a completely different schedule.

The six fields of an Elasticsearch cron expression: seconds, minutes, hours, day-of-month, month, day-of-weekThe expression 0 30 1 star star question mark is split into six labelled columns. Left to right: 0 is seconds, 30 is minutes, 1 is hours, the first star is day-of-month, the second star is month, and the question mark is day-of-week meaning no specific value. A note explains the leading seconds field is absent from a five-field Unix crontab, which is the usual source of error.schedule: "0 30 1 * * ?" · daily at 01:30 UTC0seconds30minutes1hours*day-of-month*month?day-of-weekthis leading field is absent from a 5-field Unix crontab — the usual mistake? = "no specific value" (day-of-month already pinned)

Two schedules cover most needs:

  • Daily at 01:30 UTC: 0 30 1 * * ? — second 0, minute 30, hour 1, any day, any month, no day-of-week constraint.
  • Every hour on the half-hour: 0 30 * * * ? — the hours field becomes *, so it fires at 00:30, 01:30, 02:30, and so on.

The ? in the day-of-week field is not decoration. Elasticsearch cron does not allow both day-of-month and day-of-week to carry a concrete value at once, because the two would combine ambiguously; when you pin the day-of-month (or use * for it, as here), you set day-of-week to ? to mean “no specific value.” Setting both to * is a common way to trigger a validation error.

The date-math name template

Each snapshot in a repository needs a distinct name, so the policy’s name field is a date-math template rather than a literal string. The template is wrapped in angle brackets and embeds an expression like {now/d}:

  • <daily-logs-{now/d}> rounds the current time down to the day, producing daily-logs-2026.07.17 for any fire on 17 July.
  • <hourly-logs-{now/h}> rounds to the hour, producing hourly-logs-2026.07.17.08 for the 08:xx fire.

Elasticsearch also appends its own random suffix beneath the rounded name, so even two fires that land in the same rounding bucket cannot collide. Match the rounding unit to the cadence: a daily schedule with {now/h} rounding wastes resolution, while an hourly schedule with {now/d} rounding would produce names that only differ by the random suffix and are miserable to read in a _cat/snapshots listing.

The angle brackets matter because they signal date-math to Elasticsearch. When you send the policy through a raw HTTP client or a URL, the < and > characters must be URL-escaped (%3C and %3E); the elasticsearch-py client and Kibana Dev Tools handle this for you, which is one more reason to apply policies through a maintained client rather than hand-rolled curl.

Implementation

Apply the policy with the schedule and name template in place. Here is a daily policy over HTTP:

PUT _slm/policy/daily-logs-backup
{
  "schedule": "0 30 1 * * ?",
  "name": "<daily-logs-{now/d}>",
  "repository": "prod-s3-repo",
  "config": { "indices": ["logs-*"], "include_global_state": false }
}

The same operation from the elasticsearch-py v8 client, which escapes the date-math brackets and surfaces failures as ApiError:

import logging
from elasticsearch import Elasticsearch, ApiError

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("slm_schedule")


def schedule_snapshots(es: Elasticsearch, policy_id: str, cron: str, name_template: str) -> bool:
    policy = {
        "schedule": cron,                 # six-field Elasticsearch cron, UTC
        "name": name_template,            # date-math template, e.g. <daily-logs-{now/d}>
        "repository": "prod-s3-repo",
        "config": {"indices": ["logs-*"], "include_global_state": False},
    }
    try:
        es.slm.put_lifecycle(policy_id=policy_id, body=policy)
        # Read back the armed next-fire time so a wrong cron is caught immediately.
        record = es.slm.get_lifecycle(policy_id=policy_id)[policy_id]
        logger.info("Scheduled '%s'; next_execution=%s", policy_id, record.get("next_execution"))
        return True
    except ApiError as exc:
        logger.error("Scheduling failed (%s): %s", exc.status_code, exc.info)
        return False


if __name__ == "__main__":
    es = Elasticsearch("https://es-cluster-01:9200", api_key="YOUR_API_KEY", verify_certs=True)
    schedule_snapshots(es, "daily-logs-backup", "0 30 1 * * ?", "<daily-logs-{now/d}>")

Before trusting the cadence, fire the policy once by hand — this validates the whole path without waiting hours for the next scheduled tick:

POST _slm/policy/daily-logs-backup/_execute

A returned snapshot_name such as daily-logs-2026.07.17-9f3c2a1b confirms both the schedule applied and the name template renders correctly.

Staggering multiple policies

When several policies back different index sets into the same prod-s3-repo, do not point them all at the same minute. Concurrent snapshots contend for the same upload bandwidth to object storage, and a thundering herd at 01:30 turns a set of individually-cheap snapshots into one saturating I/O spike. Offset them — 0 30 1 * * ?, 0 30 2 * * ?, 0 30 3 * * ? — so heavy incremental copies never overlap. The bandwidth ceiling itself is tunable via snapshot.max_snapshot_bytes_per_sec, but spreading start times is the cheaper first move.

Verification

Read the policy and confirm the schedule is armed with a sensible next-fire time:

GET _slm/policy/daily-logs-backup
{
  "daily-logs-backup": {
    "policy": { "schedule": "0 30 1 * * ?", "name": "<daily-logs-{now/d}>" },
    "next_execution": "2026-07-18T01:30:00.000Z",
    "next_execution_millis": 1752802200000
  }
}

The next_execution timestamp is the definitive check: if it does not read as the time you intended (note the trailing Z — it is UTC), the cron expression means something other than you think. Then, after the first fire, confirm the produced names sort chronologically:

GET _cat/snapshots/prod-s3-repo?v&h=id,status,start_epoch&s=start_epoch:desc

Gotchas and edge cases

  • The seconds field is real — count six. The most common failure is treating the expression as a five-field crontab. Always read left to right as seconds minutes hours day-of-month month day-of-week; if your expression has five fields it is wrong for Elasticsearch even though it is valid Unix cron.
  • Keep the angle brackets and date-math. A name of daily-logs without <...> and {now/d} produces a literal, non-unique name; the second fire then collides or is disambiguated only by the hidden random suffix, defeating the point of a readable naming scheme.
  • Snapshot schedule is not retention schedule. The schedule field only governs when snapshots are taken. Old snapshots are deleted by the separate cluster-wide slm.retention_schedule, so tightening the snapshot cadence does not tighten pruning — configure retention separately.
  • Times are UTC by default. Elasticsearch evaluates SLM cron in the deployment’s timezone, which is UTC unless deliberately changed. A schedule of 0 30 1 * * ? fires at 01:30 UTC, not 01:30 in your local zone; convert deliberately when a runbook specifies a wall-clock window.

FAQ

Why does my SLM cron expression have six fields?

Elasticsearch cron prepends a seconds field, giving the order seconds minutes hours day-of-month month day-of-week. A standard Unix crontab has only the last five. If you paste a five-field expression, every value shifts one position and the schedule fires at the wrong time, so always confirm you have six fields and that the first one is seconds.

What does the question mark in the day-of-week field mean?

? means “no specific value.” Elasticsearch cron will not let both day-of-month and day-of-week carry a concrete value simultaneously, because the combination is ambiguous. When day-of-month is set (or *), put ? in day-of-week to signal you are not constraining it; setting both to * can trigger a validation error.

How do I schedule hourly instead of daily snapshots?

Change the hours field to * and the rounding unit in the name template to hours: "schedule": "0 30 * * * ?" with "name": "<hourly-logs-{now/h}>". That fires at 30 minutes past every hour and names each snapshot by the hour so the listing stays readable. Remember hourly snapshots still prune on the once-a-day retention schedule unless you also adjust slm.retention_schedule.

← Back to Configuring SLM Policies · SLM & Searchable Snapshots