serverless

EventBridge cannot tell you when the sun sets

EventBridge cannot tell you when the sun sets

I wanted the lamps in the living room to come on at dusk. Not at nine o'clock — at dusk, which in Sofia means quarter past nine in June and half past four in December.

EventBridge schedules are cron expressions. Cron expressions are fixed. This is a longer problem than it looks.

The three ways out

Recompute the schedule. A nightly job works out tomorrow's sunset and calls PutRule to rewrite the cron expression. It is exact, and it is a moving part that can fail silently at two in the morning and leave the flat dark for a day before I notice. It also means a Lambda function with IAM permission to rewrite its own infrastructure, which I did not love.

A one-minute schedule and a check. Correct, wasteful, and the waste is mostly in my head rather than in the bill.

A coarse schedule and a window. Run every fifteen minutes, work out how far away sunset is, and do nothing unless it is close. This is what I went with.

def within_window(kind, window_minutes, now=None):
    delta = minutes_until(kind, now)
    if delta is None:
        return False
    return 0 <= delta < window_minutes

The handler is then almost nothing:

def lambda_handler(event, context):
    if not suntimes.within_window(suntimes.SUNSET, WINDOW_MINUTES):
        return {"applied": []}
    ...

Ninety-five times out of ninety-six it returns in single-digit milliseconds. Two of these functions, one for each end of the day, come to about seventy thousand invocations a year. The free tier is a million a month. The cost of being imprecise here is genuinely nothing.

There are two things to get right. The window has to be at least as long as the schedule interval, or a day will slip through the gap between two runs. And because the window spans several runs, the job fires more than once — so it has to be safe to run twice, which for "set these three lamps to these values" it is.

Forty lines of trigonometry

That leaves the actual question: when is sunset? In 2021 the options were a Lambda layer for a small astronomy package, or the sunrise equation, which is about forty lines and no dependency at all. For switching a lamp on, a minute either way does not matter.

def utc_time(kind, day=None, latitude=None, longitude=None):
    """Return the UTC datetime of sunrise or sunset, or None on a polar day."""
    day_of_year = day.timetuple().tm_yday
    longitude_hour = longitude / 15.0

    base = 6.0 if kind == SUNRISE else 18.0
    approximate = day_of_year + (base - longitude_hour) / 24.0

    mean_anomaly = 0.9856 * approximate - 3.289
    true_longitude = _mod(
        mean_anomaly
        + 1.916 * math.sin(math.radians(mean_anomaly))
        + 0.020 * math.sin(math.radians(2 * mean_anomaly))
        + 282.634,
        360.0,
    )
    ...

Two details in there caught me out. The right ascension has to be pushed into the same quadrant as the true longitude, or the answer is wrong by a neat multiple of six hours — which looks so plausible that I nearly shipped it. And the hour angle can come out beyond ±1 before the arc cosine, which is not a bug but the polar day: above the Arctic Circle there is no sunset to return. Sofia will never hit that branch. It still returns None rather than throwing, on the grounds that a lamp failing to switch on is better than a function failing to run.

Checked against the real thing for 2021:

2021-06-21  rise 02:49 UTC  set 18:08 UTC   # 05:49 and 21:08 local
2021-12-21  rise 05:53 UTC  set 14:56 UTC   # 07:53 and 16:56 local

Both within a minute of the almanac. Everything inside the function is UTC, and the only place local time appears is the evening lighting rule, which asks whether the hour is between seven and seven.

Why not a scheduler

The neat version of this is a service that knows about wall time, sunset, holidays and my calendar, and fires exactly once. That service is a project. What I have is a cron expression, a window, and forty lines of maths — and the lamps come on at dusk, which was the whole ask.

Deyan Peev

Written by

Deyan Peev

Founding Engineer · Sofia, Bulgaria

Deyan Peev

Founding Engineer in Sofia, Bulgaria. Currently at 1club.

Elsewhere

© 2026 Deyan Peev