How to Schedule Website Screenshots Around Daylight Saving Time Changes
Keep screenshot schedules at the intended local time through daylight saving changes. Choose a time zone, define gap and overlap behavior, and test both transitions.
To keep a daily screenshot at 09:00 local time, schedule it as a recurring local calendar time in a named time zone, such as America/New_York. Define what happens if the scheduled time is skipped when clocks move forward or occurs twice when clocks move back. Do not calculate each run as the previous run plus 24 elapsed hours, or reuse one fixed UTC offset.
A screenshot schedule can mean either “09:00 every day in this region” or “every 24 elapsed hours.” Those are different schedules around daylight saving time (DST). Choose the one that matches the capture’s purpose, then verify the scheduler’s documented time-zone and retry behavior. This guide covers the date and time logic; it does not assume any particular scheduler or screenshot provider supports a given configuration.
1. Choose what the schedule means
For a business-hours report, you usually want a local wall-clock recurrence: capture at the same displayed time in a particular region, even as its UTC offset changes. For a process that must run at a fixed elapsed interval, such as every 24 hours after a previous event, use elapsed-time scheduling and accept that its local display time can shift at DST transitions.
| Schedule intent | How to calculate | What DST can change |
|---|---|---|
| 09:00 every day in a region | Recompute each occurrence from local calendar date, local time, and named zone | The corresponding UTC time changes when the zone’s offset changes |
| Every 24 elapsed hours | Add 24 hours to the prior instant | The displayed local time may move by an hour |
2. Store the named zone and transition policy
Store the IANA time-zone identifier alongside the local recurrence and the policy for exceptional dates. For example, a schedule record might contain:
{
"local_time": "09:00",
"time_zone": "America/New_York",
"recurrence": "daily",
"missing_time_policy": "skip",
"repeated_time_policy": "once_earlier"
}
Choose a zone appropriate to the schedule, not whichever zone the server happens to use. A named zone carries regional rules; a fixed offset such as -05:00 does not capture future transitions or rule changes. The offset depends on both the zone and the instant. See [MDN’s Temporal.ZonedDateTime reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime).
Decide how to resolve the spring gap and autumn overlap
When clocks move forward, some local times do not exist. If a schedule says 02:30 on a date when the clock jumps from 01:59 to 03:00, there is no matching instant. Decide whether to skip that run or move it to a valid time, and document the choice.
When clocks move backward, some local times occur twice. Decide whether to capture once at the earlier occurrence, once at the later occurrence, or twice. “Run once” is often a sensible operational policy, but the correct choice depends on the job.
Temporal’s documented “compatible” disambiguation chooses a later time for a gap and an earlier occurrence for an overlap in the relevant date-time operations. That is an API behavior, not a universal scheduling policy. Use an explicit policy that matches your requirements. [MDN documents Temporal’s transition behavior](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime/add).
3. Resolve local times explicitly in Python
This runnable standard-library example resolves a local date and time in an IANA zone. It detects nonexistent and repeated times by checking whether each possible fold value survives a UTC round trip. The sample policy skips nonexistent times and selects the earlier instant for repeated times. Change those decisions to match your scheduler’s needs.
from datetime import date, datetime, time, timezone
from zoneinfo import ZoneInfo
def resolve_local(when: date, at: time, zone_name: str,
missing: str = "skip", repeated: str = "earlier"):
"""Return a UTC datetime, or None when a missing time is skipped."""
zone = ZoneInfo(zone_name)
naive = datetime.combine(when, at)
candidates = []
for fold in (0, 1):
local = naive.replace(tzinfo=zone, fold=fold)
utc = local.astimezone(timezone.utc)
round_trip = utc.astimezone(zone).replace(tzinfo=None)
if round_trip == naive and all(utc != item for item in candidates):
candidates.append(utc)
candidates.sort()
if not candidates:
if missing == "skip":
return None
if missing == "forward":
# Find the first valid local minute after the requested time.
probe = naive
for _ in range(180):
probe = probe.replace() # retain a naive local datetime
probe += __import__("datetime").timedelta(minutes=1)
result = resolve_local(probe.date(), probe.time(), zone_name,
missing="skip", repeated=repeated)
if result is not None:
return result
raise ValueError("No valid local time found within three hours")
raise ValueError("missing must be 'skip' or 'forward'")
if len(candidates) == 1:
return candidates[0]
if repeated == "earlier":
return candidates[0]
if repeated == "later":
return candidates[-1]
if repeated == "twice":
return candidates
raise ValueError("repeated must be 'earlier', 'later', or 'twice'")
# Example: a daily 09:00 capture in New York.
run = resolve_local(date(2026, 11, 2), time(9, 0), "America/New_York")
print(run.isoformat() if run else "Skipped by policy")
For production use, the example’s twice policy returns two UTC datetimes; the caller must enqueue both. The forward policy advances minute by minute to the first valid local minute and is included for clarity, not as a recommendation for every workflow. For recurring schedules, generate each local calendar date independently and resolve it; do not derive tomorrow by adding 24 hours to today’s UTC instant.
4. Keep JavaScript host-zone behavior out of the schedule
JavaScript Date local getters, setters, and getTimezoneOffset() use the host runtime’s local zone. A server may run in UTC while the schedule is intended for another region. Constructing host-local dates can therefore silently schedule the wrong capture. A datetime-local form field also contains no zone identifier, and can accept a local time that does not exist in the chosen zone. Collect and store the zone separately. [MDN explains Date local-time behavior](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) and [datetime-local’s zone limitation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local).
Where your runtime supports Temporal, use an explicit ZonedDateTime and calendar-day arithmetic for local daily recurrences. Adding one calendar day can preserve the wall-clock time across DST; adding 24 hours advances elapsed time and can produce a different local clock reading. Temporal support is not universal across widely used browsers, so check compatibility before relying on it in browser code. [MDN’s Temporal compatibility and arithmetic documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime).
// In a runtime with Temporal support. Check compatibility before deployment.
const zone = "America/New_York";
const first = Temporal.ZonedDateTime.from(
"2026-11-01T09:00-05:00[America/New_York]"
);
const nextLocalDay = first.add({ days: 1 });
console.log(nextLocalDay.toString());
// This is elapsed-time arithmetic and has different recurrence semantics:
const next24Hours = first.add({ hours: 24 });
console.log(next24Hours.toString());
For a recurring job, also apply your chosen gap and overlap policy when creating each occurrence. Do not assume a date-time library’s default ambiguity handling is the same as the scheduler’s contract.
5. Configure and test the scheduler
- Set a named IANA time zone in the scheduler, if it supports one. If it only accepts UTC, calculate each intended occurrence from local calendar fields and the zone’s rules, then enqueue the resulting UTC instant.
- Record the local recurrence, zone identifier, missing-time policy, and repeated-time policy in configuration or job metadata.
- Confirm from the scheduler’s current documentation how it handles gaps, overlaps, retries, duplicate executions, and time-zone database updates. These details vary by provider; this guide does not establish guarantees for any specific scheduler.
- In staging, schedule a time inside a known spring gap and a time inside a known autumn overlap. Check the calculated UTC instant, displayed local time, and actual number of screenshot jobs.
- Log both the intended local occurrence and resolved UTC instant with each run. This makes an unexpected shift or duplicate easier to diagnose.
For a concrete transition example, New York skipped from 01:59 to 03:00 on 2024-03-10, and repeated the 01:00 hour on 2024-11-03. These dates illustrate the gap and overlap problem; use the rules for your chosen zone and scheduled date. [MDN’s Temporal transition guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime).
6. Capture the page when the scheduled job runs
Once the scheduler has resolved an occurrence to an instant, the capture step can call your screenshot method. The capture API does not decide whether a recurrence is local-time or elapsed-time; keep that logic in the scheduler. The following direct request uses ScreenshotNeo’s documented API shape. Put the key in a secret store or environment variable in a real application, and consult the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as output:
output.write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
For scheduled runs, make the capture job safe to retry: associate it with the resolved occurrence, track whether it has completed, and avoid creating an unintended second capture if the scheduler redelivers a job. The exact retry and idempotency mechanisms depend on your scheduler and application.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. It handles the screenshot request after your schedule determines when to run. Its capture flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and responses include X-Page-Verdict and X-Billed headers. Its MCP server gives AI agents tools named take_screenshot, get_page_info, and capture_pdf. Plans include 1,000 screenshots per month free with no card, then paid plans from $5 for 3,000; every feature is on every plan. See [ScreenshotNeo](https://screenshotneo.com) and the [API documentation](https://screenshotneo.com/docs/).
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000. Create a free account.
Reliability, performance, and cost considerations
- Reliability: Persist the zone and policy, log each resolved instant, and verify duplicate and retry behavior in the scheduler. Recheck transition tests after time-zone data or runtime updates when accurate local timing matters; governments can change zone rules.
- Performance: Resolve future occurrences when needed rather than repeatedly recalculating them for every worker. Keep capture execution separate from recurrence calculation so a slow screenshot does not shift the next local run. For high-volume schedules, batch calculations and queue work with explicit occurrence identifiers.
- Cost: DST handling itself does not make a capture more expensive, but a repeated-time policy of “twice” creates an additional run. Skipped or shifted occurrences change the number and timing of captures. For ScreenshotNeo pricing, the free tier is 1,000 shots/month; paid monthly plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. Every feature is on every plan. Budget based on intended captures plus any deliberate duplicate runs.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot shifts by an hour after DST | The schedule adds a fixed 24-hour interval or reuses one UTC offset | Recompute each date from local calendar fields and a named zone, or choose elapsed-time semantics explicitly. |
| The job runs at the wrong local time on a server | Code uses the server’s host zone through JavaScript Date local methods |
Use the intended IANA zone explicitly; do not infer it from the host. |
| A spring transition run is missing | The configured local time falls inside the skipped interval | Set and test a gap policy: skip, move forward, or another documented resolution. |
| A fall transition creates two captures | The local time occurs twice and the scheduler triggers both instants | Choose earlier, later, or twice behavior and deduplicate if only one capture is intended. |
| The date-time form accepts an impossible time | datetime-local contains no time-zone information and does not validate against a regional zone’s DST rules |
Collect the IANA zone separately and validate the selected date-time in that zone on the server. |
| Temporal code fails in a browser | The browser does not support the API | Check target-runtime compatibility and use a supported explicit-zone library or server-side resolver. |
| The capture ran twice after a retry | The scheduler redelivered a job or the overlap policy selected both instants | Track a stable occurrence ID and make retry processing idempotent; inspect the scheduler’s retry semantics. |
FAQ
Should I store UTC or local time?
Store the recurrence’s local time and IANA zone as the schedule definition. Store each resolved UTC instant as an execution record so it can be queued and audited.
Is a fixed UTC offset enough?
Only if the schedule intentionally follows that fixed offset. It cannot represent changing regional DST rules or future government rule changes.
Does every screenshot provider support DST-aware schedules?
This research does not verify any provider’s scheduler behavior. Confirm named-zone support, gap and overlap semantics, retries, and duplicate handling in the provider’s current documentation.
Does a daily local schedule always run exactly 24 hours apart?
No. A local calendar recurrence preserves the intended wall-clock time, while the elapsed interval between executions can differ around a transition.


