Unix-timestamps i planlagte opgaver og cron-jobs

Cron-jobs er simple på overfladen: kør denne kommando på dette tidspunkt. Men planlægning i produktion bliver hurtigt kompliceret. Du skal spore, hvornår et job sidst kørte, opdage om det er forsinket, håndtere overlappende kørsler og logge alt med nok præcision til at kunne debugge fejl bagefter. Unix-timestamps er et praktisk værktøj til alt dette.

Brug Unix Timestamp Converter til at konvertere en timestamp til en læsbar dato eller til at få den aktuelle Unix-tid. Denne artikel gennemgår, hvordan timestamps bruges i planlægningssystemer — fra basic cron til mere komplekse jobkøer.

Hvorfor planlagte opgaver bruger Unix-timestamps

Et cron-udtryk som 0 2 fortæller en scheduler hvornår* et job skal køre. Men det siger intet om, hvad der skete under kørslen. Til det har du brug for timestamps, der bliver gemt ved runtime: hvornår jobbet startede, hvornår det sluttede, og om det lykkedes.

Unix-timestamps er et naturligt format, fordi de er:

  • Entydige — ingen tidszoneforvirring, ingen locale-afhængig formatering
  • Sammenlignelige — du kan trække to timestamps fra hinanden og få forløbne sekunder
  • Kompakte — ét heltal kan gemmes i enhver databasekolonne eller loglinje
  • Sorterbare — heltalsorden svarer til kronologisk orden

En planlagt opgave, der skriver started_at: 1712534400 til en database, gemmer noget præcist og forespørgbart. En opgave, der skriver started_at: "April 8, 2026 at 2:00 AM", gemmer noget, der kræver parsing, før det er nyttigt.

Gemme timestamps for sidste kørsel

Den mest almindelige brug af timestamps i planlagte opgaver er at spore sidste vellykkede kørsel. Et simpelt mønster:

1. Job starter og læser last_run-timestamp fra storage 2. Job udfører sit arbejde 3. Ved succes skriver job den aktuelle Unix-timestamp til last_run 4. Ved fejl lader job last_run være uændret (eller skriver til et separat last_failed_at-felt)

# Shell example
LAST_RUN=$(cat /var/run/myjob.timestamp 2>/dev/null || echo 0)
NOW=$(date +%s)

# Do work here...
if [ $? -eq 0 ]; then
    echo $NOW > /var/run/myjob.timestamp
fi

Dette mønster gør det trivielt at svare på “hvornår lykkedes dette job sidst?” — læs bare filen og konvertér timestampen. Det gør det også nemt at opdage staleness: hvis now - last_run > expected_interval, er jobbet forsinket.

Opdage forsinkede eller missede jobs

Cron opdager ikke sine egne fejl. Hvis en server er nede under en planlagt kørsel, forsøger cron ikke igen og alarmerer ikke. Hvis et job kører men slutter med en fejl, markerer cron det ikke som failed. For at opdage det kræves ekstern monitorering, der tjekker timestamps.

En simpel check: hvis den aktuelle tid minus last_run overstiger en tærskel, er noget gået galt.

import time

EXPECTED_INTERVAL_SECONDS = 3600  # job should run hourly
TOLERANCE_SECONDS = 300           # allow 5 minutes of drift

last_run = get_last_run_timestamp()  # from DB or file
now = int(time.time())

if now - last_run > EXPECTED_INTERVAL_SECONDS + TOLERANCE_SECONDS:
    alert("Job overdue — last ran at {}".format(last_run))

Tolerancen er vigtig. Cron-jobs starter ikke altid præcist til tiden — system load, clock drift og startup betyder, at et job planlagt til 02:00:00 måske starter 02:00:04. Uden tolerance kan en monitoreringscheck kl. 02:59:56 fejlagtigt tro, at jobbet er forsinket, selvom det kørte fint 02:00:04 og først er “due” igen om 4 sekunder.

For timejobs er 5 minutter en rimelig tolerance. For daglige jobs er 30–60 minutter typisk.

Undgå overlappende kørsler med timestamps

Langt kørende jobs kan overlappe, hvis næste planlagte kørsel starter, før den nuværende er færdig. Et dagligt backup-job, der tager 2 timer, er fint. Et, der tager 26 timer, begynder at overlappe og kan til sidst skabe kaskadefejl.

Timestamps løser det med et simpelt lock-mønster:

1. Job starter og læser started_at fra en lock-record 2. Hvis started_at findes og now - started_at < timeout, kører en anden instans — afslut 3. Hvis der ikke er nogen lock eller lock er udløbet, skriv nuværende timestamp som started_at 4. Udfør arbejdet 5. Ryd lock ved afslutning

LOCK_TIMEOUT = 7200  # 2 hours — if job runs longer, assume it's stuck

lock_time = get_lock_timestamp()
now = int(time.time())

if lock_time and (now - lock_time) < LOCK_TIMEOUT:
    print("Job already running, started at {}".format(lock_time))
    exit(0)

set_lock_timestamp(now)
# ... do work ...
clear_lock()

Timeout håndterer tilfælde, hvor et job crasher uden at rydde sin lock. Uden timeout ville et crashet job blokere alle fremtidige kørsler for altid. Med en timestamp-baseret lock udløber locken efter timeout, og næste planlagte kørsel kan fortsætte.

Planlægge fremtidige jobs med timestamps

Jobkøer (Sidekiq, Celery, BullMQ, RQ) planlægger ofte fremtidige tasks ved at gemme en Unix-timestamp for, hvornår jobbet skal eksekveres. Worker’en poller efter jobs hvor run_at <= current_timestamp.

-- Find jobs ready to run
SELECT * FROM scheduled_jobs
WHERE run_at <= EXTRACT(EPOCH FROM NOW())::int
  AND status = 'pending'
ORDER BY run_at ASC;

Det er mere fleksibelt end cron til dynamisk planlægning. I stedet for “kør hver time” kan du sige “kør 24 timer efter brugeren tilmelder sig” eller “kør 15 minutter efter denne betaling fejlede.” Jobbet indsættes med run_at = NOW_UNIX + delay_seconds, og worker’en tager det, når tiden kommer.

Retry-logik fungerer også sådan. Efter en fejl kan du reschedule med eksponentiel backoff:

attempt = job.attempt_count
delay = min(2 ** attempt * 60, 3600)  # 1min, 2min, 4min... up to 1hr
job.run_at = int(time.time()) + delay
job.save()

Efter 1 forsøg: retry om 60 sekunder. Efter 2: 120 sekunder. Efter 3: 240 sekunder. Cap på 3600 forhindrer uendelige forsinkelser.

Logge jobkørsler med Unix-timestamps

Job-logs er mest nyttige, når de inkluderer præcis timing. En loglinje som:

[1712534400] backup-job started
[1712534447] backup-job completed in 47s, 3.2GB written

er med det samme brugbar til debugging. Du kan konvertere 1712534400 til en læsbar dato med Unix Timestamp Converter, korrelere med andre systemlogs og beregne præcis varighed uden at parse datostringe.

Alternativet — at logge formaterede strenge som "2026-04-08 02:00:00 UTC" — er læsbart for mennesker, men skrøbeligt for maskiner. Forskellige log shippers formaterer datoer forskelligt, tidszonekonverteringer introducerer fejl, og string-sammenligning er langsommere end heltals-sammenligning ved tidsinterval-forespørgsler.

Et almindeligt mønster er at logge begge dele: rå timestamp for maskinlæsbarhed og formateret dato for menneskelæsbarhed.

started_at=1712534400 started_at_human="2026-04-08T02:00:00Z" duration_s=47

Måle varighed og performance over tid

Timestamps gør det muligt at tracke performance på tværs af kørsler. Gem started_at og completed_at for hver kørsel, og du kan forespørge:

  • Gennemsnitlig varighed over de sidste 30 kørsler
  • Længste kørsel i den sidste uge
  • Om varigheden er på vej op (mulig performance regression)
SELECT
    AVG(completed_at - started_at) AS avg_duration_seconds,
    MAX(completed_at - started_at) AS max_duration_seconds,
    COUNT(*) AS run_count
FROM job_runs
WHERE job_name = 'nightly-report'
  AND started_at > EXTRACT(EPOCH FROM NOW())::int - (30 * 86400);

Den slags query er kun muligt, fordi varigheder er simpel heltals-subtraktion. Hvis du havde gemt formaterede datoer, skulle du parse strenge før aritmetik.

Timestamp-præcision: sekunder vs millisekunder i planlægning

For de fleste planlagte opgaver er sekund-præcision nok. Et job planlagt til 1712534400 behøver ikke sub-sekund nøjagtighed.

Men for højfrekvente køer — jobs, der kan køre hundredevis af gange per sekund — betyder millisekund-timestamps noget. Hvis to jobs indsættes i samme sekund, og du sorterer efter timestamp for at bestemme rækkefølge, giver sekund-præcision ties. Millisekund-timestamps bevarer insertion order inden for hvert sekund.

JavaScripts Date.now() returnerer millisekunder. Pythons time.time() returnerer en float med mikrosekund-præcision. De fleste databaser understøtter mikrosekund-præcise timestamps. Valg af præcision bør matche din planlægningsfrekvens — millisekunder til højvolumen-køer, sekunder til standard cron-lignende jobs.

Håndtere sommertid (DST) i planlagte opgaver

Det er her mange planlægningsbugs gemmer sig. Et cron-job konfigureret som 0 2 * kører kl. 02:00 lokal tid. Når uret springer frem, findes 02:00 ikke — uret går fra 01:59 til 03:00. Jobbet bliver enten sprunget over eller kørt 03:00 afhængigt af cron-implementeringen.

Når uret går tilbage, forekommer 02:00 to gange. Jobbet kan køre to gange.

Løsningen: kør cron i UTC. 0 2 * i UTC er altid 02:00 UTC — ingen tvetydighed, ingen DST-overraskelser. Jobtidspunktet flytter sig relativt til lokal tid to gange om året, men det kører altid præcis én gang.

Unix-timestamps er UTC per definition, hvilket er grunden til, at de er immune over for dette problem. Et job planlagt til timestamp 1712534400 kører på det præcise tidspunkt uanset serverens tidszone eller DST-offset på det tidspunkt. Det er kerneargumentet for at bruge timestamp-baserede schedulers frem for cron til alt, hvor præcis timing betyder noget.

For jobs hvor lokal tid-semantik betyder noget — “kør kl. 9 i business hours” — har du brug for eksplicit tidszonehåndtering, ikke bare UTC. Gem target-tidszone sammen med planen, konvertér til UTC ved oprettelse, og genberegn hvis tidszonens DST-regler ændrer sig.