windmill-labs/windmill · error
Invalid time value
Error message
Invalid time value
What it means
PostgreSQL TIMETZ values are decoded manually: microseconds since midnight plus a UTC offset are normalized to a seconds-of-day count, then chrono::NaiveTime::from_num_seconds_from_midnight_opt builds the time. That constructor returns None when the seconds value exceeds 86399 or the nanosecond value exceeds 1_999_999_999, and the code maps that to "Invalid time value". This happens only for corrupt/out-of-range wire data, since the modulo (utc_sec + 3600*24) % (3600*24) normally keeps seconds in range.
Source
Thrown at backend/windmill-worker/src/pg_executor.rs:2084
fn accepts(ty: &Type) -> bool {
matches!(ty, &Type::INTERVAL)
}
}
struct TimeTZStr(String);
impl<'a> FromSql<'a> for TimeTZStr {
fn from_sql(
_: &Type,
mut raw: &'a [u8],
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
let microsecond = raw.get_i64();
let offset = raw.get_i32();
let utc_sec = (microsecond / 1_000_000) + offset as i64;
let utc = chrono::NaiveTime::from_num_seconds_from_midnight_opt(
((utc_sec + 3600 * 24) % (3600 * 24)) as u32,
((microsecond % 1_000_000) * 1_000) as u32,
)
.ok_or_else(|| anyhow::anyhow!("Invalid time value"))?;
// ISO-8601: append `+00:00` since TIMETZ is normalised to UTC here.
Ok(TimeTZStr(format!("{}+00:00", utc)))
}
fn accepts(ty: &Type) -> bool {
matches!(ty, &Type::TIMETZ)
}
}
/// Format a `NaiveDateTime` as ISO-8601 (`YYYY-MM-DDTHH:MM:SS[.fff…]`).
/// chrono's default `to_string` uses a space separator, which is not parseable
/// by `new Date(s)` in older JS engines or Python's `datetime.fromisoformat`
/// before 3.11. Use the explicit format string so output is portable.
fn format_naive_datetime_iso(dt: &chrono::NaiveDateTime) -> String {
if dt.and_utc().timestamp_subsec_nanos() == 0 {
dt.format("%Y-%m-%dT%H:%M:%S").to_string()
} else {
dt.format("%Y-%m-%dT%H:%M:%S%.f").to_string()View on GitHub (pinned to e474e8803c)
Solutions
- Inspect the offending value in Postgres (SELECT column, column::text FROM ... WHERE ...) and fix/normalize the stored TIMETZ.
- Cast the column to text in the query (SELECT tz_col::text) so the raw string is returned and the binary decode path is skipped.
- Cast to time (SELECT tz_col::time) to drop the timezone offset, which uses the well-tested NaiveTime path.
- If you control ingestion, store TIMESTAMPTZ instead of TIMETZ, which avoids this custom decoder entirely.
Example fix
-- before SELECT meeting_at FROM schedules; -- meeting_at is TIMETZ -- after SELECT meeting_at::text AS meeting_at FROM schedules;
Defensive patterns
Strategy: validation
Validate before calling
-- verify stored TIMETZ values are in the valid range before querying them from a job SELECT id FROM t WHERE meeting_at < time '00:00:00+00' OR meeting_at > time '23:59:59.999999+00' OR meeting_at IS NOT NULL AND extract(epoch from meeting_at) NOT BETWEEN 0 AND 86399;
Try / catch
try {
return await query('SELECT meeting_at FROM schedules');
} catch (e) {
if (String(e.message).includes('Invalid time value')) {
return await query('SELECT meeting_at::text AS meeting_at FROM schedules');
}
throw e;
} Prevention
- Cast TIMETZ columns to ::text or ::time in job queries
- Prefer TIMESTAMPTZ over TIMETZ for new schemas
- Validate/normalize time data at ingestion rather than at read time
When it happens
Trigger: Decoding a TIMETZ column whose decoded microsecond field is negative or larger than a day's worth of microseconds in a way the normalization doesn't absorb (e.g. microsecond negative enough that (microsecond/1_000_000 + offset) before the modulo is < -86400), producing a negative seconds value that NaiveTime rejects.
Common situations: Extreme TIMETZ offsets combined with times near midnight; data written by non-Postgres tools or binary replication that encodes time-of-day unconventionally; a Postgres instance/extension emitting microsecond fields outside the documented range; decoding rows fetched by a Windmill PostgreSQL script.
Related errors
- Server mode requires a database connection
- Unexpected NULL `schema_name` in publication entry (pubname:
- Unexpected NULL `table_name` for schema `{}` in publication
- You must at least choose schema to fetch table from
- Cannot convert decimal to json
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/cf9d5d1fffb49b2a.
Report an issue: GitHub.