<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>CronJob · XB Field Notes</title><link>https://hk.xbcatm.com/tags/cronjob/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/tags/cronjob/index.xml" rel="self" type="application/rss+xml"/><item><title>Kubernetes CronJobs need a history policy</title><link>https://hk.xbcatm.com/posts/kubernetes-cronjob-cleanup-hooks/</link><pubDate>Fri, 25 Feb 2028 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/kubernetes-cronjob-cleanup-hooks/</guid><description>CronJob history defaults, Job TTL, ownership and external logs provide different retention controls.</description><content:encoded>&lt;p&gt;A CronJob creates Jobs on a schedule, and each Job manages the Pods that perform the work. Finished objects can preserve useful status, but retention is bounded by several independent mechanisms. In particular, CronJobs already have default history limits. Omitting explicit history settings does not make ordinary completed runs accumulate forever.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-kubernetes-cronjob-cleanup-hooks.detail.en.svg" alt="CronJob creates Jobs that manage Pods. Finished-history count and TTL age are independent cleanup controls. Ownership affects cascading deletion; active runs and external log retention need separate policies." width="560" height="2820"&gt;&lt;figcaption&gt;Count and TTL do not guarantee minimum retention when the other permits earlier deletion. Cleanup is asynchronous, and active-job limits do not prove a business operation is safe to retry. The retained diagnostics were not rerun.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="separate-history-from-log-retention"&gt;Separate history from log retention&lt;/h2&gt;
&lt;p&gt;The default CronJob history keeps up to three successful Jobs and one failed Job. The controller reconciles those categories independently. A zero limit requests no retained Jobs in that category, but cleanup is asynchronous rather than an exact completion-time deletion. Active Jobs are not completed-history entries and need separate attention.&lt;/p&gt;
&lt;p&gt;Thirty days of hourly scheduling means 720 scheduled opportunities, not necessarily 720 retained Jobs or exactly 720 Pods. Missed schedules, concurrency policy, retries and parallelism change those relationships. A Job can own several Pods. State the assumptions before using run counts as storage or API-capacity estimates.&lt;/p&gt;
&lt;p&gt;Keeping a Pod object also does not guarantee its logs remain available. Node failure, rotation, container cleanup and eviction affect log access. Export the operational evidence required by your retention policy to a suitable logging system. Kubernetes objects are useful investigation records, but they are not an archival guarantee.&lt;/p&gt;
&lt;p&gt;Large retained histories still have costs: object storage, list sizes and investigation noise. A watch receives relevant changes rather than repeatedly emitting every unchanged object merely because it exists. Measure actual object counts and control-plane behavior instead of describing retained status as continuous activity.&lt;/p&gt;
&lt;h2 id="count-and-age-are-independent-policies"&gt;Count and age are independent policies&lt;/h2&gt;
&lt;p&gt;The CronJob history limits constrain finished Job counts, not their minimum age. A busy schedule can replace the last failed record quickly, while a quiet schedule may keep it much longer. Choose counts from the inspection interval and failure frequency, rather than treating one retained failure as enough evidence for every workload.&lt;/p&gt;
&lt;p&gt;TTL-after-finished is configured on the Job. For future CronJob-created Jobs, place ttlSecondsAfterFinished inside the Job template&amp;rsquo;s spec. The timer starts when a Job reaches Complete or Failed; expiry makes it eligible for cascading deletion. A zero TTL makes it eligible immediately, while an unset TTL supplies no age-based cleanup.&lt;/p&gt;
&lt;p&gt;History cleanup and TTL can each remove a Job. Consequently, neither a large history count nor a long TTL guarantees minimum retention when the other mechanism permits earlier deletion. Finalizers and controller progress can delay physical removal. Editing a CronJob template affects future Jobs; it does not rewrite the policy of every existing Job.&lt;/p&gt;
&lt;h2 id="ownership-determines-cascading-behavior"&gt;Ownership determines cascading behavior&lt;/h2&gt;
&lt;p&gt;CronJob-created Jobs reference their owner, and Jobs reference their Pods through dependent ownership. Normal cascading deletion can remove that hierarchy. Explicit orphan deletion instead preserves dependents and removes the relevant ownership link; it does not intentionally leave an ordinary dangling reference for the collector to follow.&lt;/p&gt;
&lt;p&gt;After orphaning Jobs, the former CronJob no longer trims their history. A configured TTL can still apply to each finished Job. Directly created Jobs also need an explicit retention decision. Check the actual deletion propagation policy and ownership rather than assuming every client uses identical defaults or that every orphan will soon disappear.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A Job that never reaches a terminal condition does not become eligible through finished-job TTL. History limits do not bound active runs either. An appropriate activeDeadlineSeconds can bound a Job&amp;rsquo;s runtime, while concurrencyPolicy controls overlapping schedules. These settings address different failure modes and do not prove the underlying operation is safe to retry.&lt;/p&gt;
&lt;p&gt;Zero history can remove the failure record before anyone investigates it. Preserve required logs and result artifacts elsewhere before shortening retention. Conversely, a missing owner relationship or an absent TTL deserves investigation, not automatic deletion: another controller or retention requirement may deliberately own the cleanup decision.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;The following retained read-only diagnostics were not rerun in this review. Select a disposable test context and namespace. Compare the stored CronJob with the Jobs it actually owns, their completion conditions and creation times; namespace-wide totals below include unrelated workloads and active Jobs.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get cronjob hourly-rollup -o jsonpath=&amp;#39;{.spec.successfulJobsHistoryLimit} {.spec.failedJobsHistoryLimit} {.spec.jobTemplate.spec.ttlSecondsAfterFinished}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get jobs --sort-by=.metadata.creationTimestamp --no-headers | wc -l
kubectl get pods --field-selector=status.phase=Succeeded --no-headers | wc -l
kubectl get jobs -o jsonpath=&amp;#39;{range .items[*]}{.metadata.name} ttl={.spec.ttlSecondsAfterFinished} owner={.metadata.ownerReferences[0].name}{&amp;#34;\n&amp;#34;}{end}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first line usually shows API-defaulted history values even when the manifest omitted them. Blank output is not proof of unlimited retention; check lookup errors and the complete object. The last line displays only the first owner reference. An absent TTL and owner mean these two particular mechanisms provide no demonstrated cleanup, not that deletion is impossible. Repeat inspection across completed runs, expecting bounded owned histories after reconciliation while active runs and unrelated objects are assessed separately.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/"&gt;Kubernetes Job&lt;/a&gt;, &amp;ldquo;Clean up finished jobs automatically&amp;rdquo; and the TTL mechanism for finished Jobs, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/"&gt;Kubernetes CronJob&lt;/a&gt;, default history limits and the job template, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/architecture/garbage-collection/"&gt;Kubernetes garbage collection&lt;/a&gt;, owner references and cascading deletion, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/"&gt;Primary reference&lt;/a&gt;, TTL eligibility and finalizers, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/cluster-administration/logging/"&gt;Primary reference&lt;/a&gt;, log rotation and node-level retention, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>A CronJob turns a schedule into Jobs</title><link>https://hk.xbcatm.com/posts/k8s-cronjob-schedules-batch-work/</link><pubDate>Fri, 17 Dec 2027 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/posts/k8s-cronjob-schedules-batch-work/</guid><description>CronJobs create Jobs on an approximate schedule; concurrency, missed-start deadlines and application idempotency determine useful batch execution.</description><content:encoded>&lt;p&gt;A CronJob combines a Job template with a recurring schedule. The CronJob controller creates Jobs; each Job manages the Pods that perform the work. A run can involve retries, replacement Pods, or parallel workers, depending on the Job specification. There need not be a dedicated application process waiting between runs, although completed Jobs and Pods may remain for inspection.&lt;/p&gt;
&lt;p&gt;Scheduling is approximate. Duplicate Jobs and missed occurrences are both possible, so do not promise unconditional at-least-once delivery for every scheduled time. Make repeated execution safe and record which business periods actually completed. Idempotency prevents duplicate effects; a separate completeness check detects work that never ran.&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/posts-k8s-cronjob-schedules-batch-work.detail.en.svg" alt="A schedule leads to a Job and its Pods. Allow permits overlap, Forbid skips a start while a Job is active, and Replace replaces the active Job, all within one CronJob. Approximate scheduling needs safe repetition and a separate check for completed business periods." width="560" height="2540"&gt;&lt;figcaption&gt;Scheduling is approximate, not a durable queue or a promise that every tick completes. startingDeadlineSeconds limits start lateness, not run duration. Source commands were not rerun; example execution remains blocked.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="a-schedule-makes-a-job"&gt;A schedule makes a Job&lt;/h2&gt;
&lt;p&gt;The five schedule fields describe minute, hour, day of month, month, and day of week. Ranges, steps, and macros such as &lt;code&gt;@daily&lt;/code&gt; are supported. &lt;code&gt;*/10 * * * *&lt;/code&gt; requests ten-minute intervals; it does not guarantee that a Pod starts precisely on those boundaries. Resource shortages and image pulls can delay execution after Job creation.&lt;/p&gt;
&lt;p&gt;Set &lt;code&gt;spec.timeZone&lt;/code&gt; explicitly when wall-clock interpretation matters. Without it, the controller manager&amp;rsquo;s local time zone applies; moving machines only changes behavior if that configuration changes. Use the timeZone field rather than putting TZ or CRON_TZ inside the schedule. Daylight-saving transitions and application reporting periods still need a deliberate policy.&lt;/p&gt;
&lt;p&gt;Keep the CronJob name within 52 characters because generated Job names need an appended suffix. Since Kubernetes 1.32, generated Jobs carry &lt;code&gt;batch.kubernetes.io/cronjob-scheduled-timestamp&lt;/code&gt;. Compare that intended time with creation and start timestamps to investigate delays. The annotation helps diagnosis; it is not the sole possible audit trail or evidence that the business operation finished.&lt;/p&gt;
&lt;h2 id="concurrency-is-scoped-to-one-cronjob"&gt;Concurrency is scoped to one CronJob&lt;/h2&gt;
&lt;p&gt;The default &lt;code&gt;Allow&lt;/code&gt; policy permits overlapping Jobs. &lt;code&gt;Forbid&lt;/code&gt; declines a new start while an earlier Job remains active; the skipped time counts as missed, and a later eligible start still depends on its deadline. &lt;code&gt;Replace&lt;/code&gt; requests deletion before creating a replacement; asynchronous deletion does not prove the old Pods have stopped. These policies do not coordinate separate CronJobs or external workers touching the same data.&lt;/p&gt;
&lt;p&gt;A task consistently longer than its interval can fail to complete under Replace. Forbid trades overlap avoidance for missed starts, rather than providing a durable queue. Use application-level deduplication or coordination where overlapping effects would be harmful, including interruption during replacement. A lock alone does not prove that a partially committed operation can safely resume.&lt;/p&gt;
&lt;h2 id="deadlines-suspension-and-history"&gt;Deadlines, suspension and history&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;startingDeadlineSeconds&lt;/code&gt; limits the lateness of a start, not execution duration. Without it, occurrences have no individual starting deadline. The concept guide describes refusing catch-up beyond 100 missed schedules. However, the reviewed 1.34.0 scheduling helper emits a warning and returns the most recent eligible time; it does not reject solely for that count. Verify the server version instead of inferring a permanent stop from the warning. A configured deadline limits the counting window, and values below ten seconds can miss controller checks.&lt;/p&gt;
&lt;p&gt;Suspension stops new Jobs without stopping existing ones. Suspended schedule times count as missed, so resuming can trigger immediate catch-up subject to the other rules. Do not treat that as a promise to replay one Job for every missed tick. Keep a durable work ledger if every reporting period must be processed.&lt;/p&gt;
&lt;p&gt;History limits retain completed Job objects, with defaults of three successes and one failure. They do not cap concurrent running Jobs or replace execution deadlines. Updating the template affects future Jobs; already-started Jobs retain their own specifications.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;A twenty-five-minute task on a ten-minute schedule can overlap under Allow and duplicate writes. Inspect actual ownership, active Pods, and completion records before attributing duplicate business effects to a schedule. A missed-start deadline should reflect how late the result remains useful, rather than an arbitrary attempt to hide controller warnings.&lt;/p&gt;
&lt;p&gt;A long gap in lastScheduleTime can also be intentional: suspension, Forbid, a sparse calendar expression, or a changed time zone may explain it. Events expire, and successful Job creation does not establish successful backups. Alert on the last verified usable result as well as scheduling health.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;In the intended read-only context and namespace, inspect an existing &lt;code&gt;nightly-report&lt;/code&gt;. These commands were retained from the earlier article and were not rerun against a cluster in this review.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get cronjob nightly-report -o jsonpath=&amp;#39;{.spec.schedule} {.spec.concurrencyPolicy} {.spec.startingDeadlineSeconds}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get cronjob nightly-report -o jsonpath=&amp;#39;{.status.lastScheduleTime}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get jobs --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,START:.status.startTime,SUCCEEDED:.status.succeeded
kubectl get events --field-selector involvedObject.kind=CronJob --sort-by=.lastTimestamp | tail -5
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The Jobs listing includes unrelated Jobs in the namespace; correlate owner references before drawing conclusions. START timestamps alone cannot prove overlap, and an empty SUCCEEDED field does not prove a Job is still running. Check active/failed counts and conditions alongside each scheduled timestamp. Interpret gaps against the actual calendar and policy, and use controller logs when retained Events lack the explanation.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/"&gt;Kubernetes CronJob&lt;/a&gt;, schedule syntax, concurrency policy, the 100-missed-schedule rule and history limits, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/docs/reference/kubernetes-api/batch/cron-job-v1/"&gt;CronJob v1 API reference&lt;/a&gt;, field-level definition of the CronJob spec, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/kubernetes/kubernetes/v1.34.0/pkg/controller/cronjob/utils.go"&gt;Kubernetes 1.34.0 CronJob scheduling helper&lt;/a&gt;, mostRecentScheduleTime and nextScheduleTime select the latest missed time and warn above 100, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/kubernetes/kubernetes/v1.34.0/pkg/controller/cronjob/injection.go"&gt;CronJob job control (v1.34.0)&lt;/a&gt;, DeleteJob uses background propagation; replacement is not a Pod-termination barrier, checked 2026-10-18.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>