[{"data":1,"prerenderedAt":705},["ShallowReactive",2],{"guide-utc-everywhere-pattern":3},{"id":4,"title":5,"body":6,"date":697,"description":698,"extension":699,"meta":700,"navigation":182,"path":701,"readingTime":173,"seo":702,"stem":703,"__hash__":704},"guides\u002Fguides\u002Futc-everywhere-pattern.md","The UTC Everywhere Pattern",{"type":7,"value":8,"toc":688},"minimark",[9,13,18,24,27,120,124,130,207,221,225,228,341,344,390,394,401,513,516,520,523,530,537,629,632,636,666,670,684],[10,11,12],"p",{},"Timezone bugs are among the most insidious in software. They do not crash your application — they silently corrupt data. A meeting scheduled for 3:00 PM shows up at 2:00 PM. A daily report runs twice or not at all. A subscription expires a day early or a day late. The root cause in nearly every case is the same: the system stored or computed time in a local timezone instead of UTC. The \"UTC Everywhere\" pattern eliminates an entire class of these bugs with one architectural rule.",[14,15,17],"h2",{"id":16},"the-core-principle","The Core Principle",[10,19,20],{},[21,22,23],"strong",{},"Store UTC. Compute in UTC. Transmit UTC. Convert to local time only at the moment of display.",[10,25,26],{},"This principle applies across every layer of your stack:",[28,29,30,46],"table",{},[31,32,33],"thead",{},[34,35,36,40,43],"tr",{},[37,38,39],"th",{},"Layer",[37,41,42],{},"What to Do",[37,44,45],{},"What to Avoid",[47,48,49,69,84,98,109],"tbody",{},[34,50,51,55,63],{},[52,53,54],"td",{},"Database",[52,56,57,58,62],{},"Store timestamps as ",[59,60,61],"code",{},"TIMESTAMPTZ"," (UTC internally)",[52,64,65,68],{},[59,66,67],{},"TIMESTAMP"," without timezone",[34,70,71,74,81],{},[52,72,73],{},"Backend",[52,75,76,77,80],{},"Accept and return ISO 8601 with ",[59,78,79],{},"Z"," suffix",[52,82,83],{},"Parsing timestamps without offsets",[34,85,86,89,95],{},[52,87,88],{},"API",[52,90,91,92],{},"Use ",[59,93,94],{},"2026-05-28T14:30:00Z",[52,96,97],{},"Passing local-time strings",[34,99,100,103,106],{},[52,101,102],{},"Frontend",[52,104,105],{},"Convert UTC to local only for rendering",[52,107,108],{},"Storing local timestamps in state",[34,110,111,114,117],{},[52,112,113],{},"Logs",[52,115,116],{},"Write timestamps in UTC",[52,118,119],{},"Logging in server-local time",[14,121,123],{"id":122},"database-layer","Database Layer",[10,125,126,127,129],{},"PostgreSQL's ",[59,128,61],{}," type stores all timestamps internally as UTC. When you read the value, it returns the timestamp converted to the session's timezone. When you write a value with an offset, it converts to UTC before storing.",[131,132,137],"pre",{"className":133,"code":134,"language":135,"meta":136,"style":136},"language-sql shiki shiki-themes github-light github-dark","-- Correct: TIMESTAMPTZ forces UTC storage\nCREATE TABLE events (\n    id SERIAL PRIMARY KEY,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    scheduled_at TIMESTAMPTZ NOT NULL\n);\n\n-- Wrong: TIMESTAMP stores the literal value, no timezone awareness\nCREATE TABLE events_bad (\n    created_at TIMESTAMP  -- Stores \"2026-05-28 14:30:00\" as-is\n);\n","sql","",[59,138,139,147,153,159,165,171,177,184,190,196,202],{"__ignoreMap":136},[140,141,144],"span",{"class":142,"line":143},"line",1,[140,145,146],{},"-- Correct: TIMESTAMPTZ forces UTC storage\n",[140,148,150],{"class":142,"line":149},2,[140,151,152],{},"CREATE TABLE events (\n",[140,154,156],{"class":142,"line":155},3,[140,157,158],{},"    id SERIAL PRIMARY KEY,\n",[140,160,162],{"class":142,"line":161},4,[140,163,164],{},"    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n",[140,166,168],{"class":142,"line":167},5,[140,169,170],{},"    scheduled_at TIMESTAMPTZ NOT NULL\n",[140,172,174],{"class":142,"line":173},6,[140,175,176],{},");\n",[140,178,180],{"class":142,"line":179},7,[140,181,183],{"emptyLinePlaceholder":182},true,"\n",[140,185,187],{"class":142,"line":186},8,[140,188,189],{},"-- Wrong: TIMESTAMP stores the literal value, no timezone awareness\n",[140,191,193],{"class":142,"line":192},9,[140,194,195],{},"CREATE TABLE events_bad (\n",[140,197,199],{"class":142,"line":198},10,[140,200,201],{},"    created_at TIMESTAMP  -- Stores \"2026-05-28 14:30:00\" as-is\n",[140,203,205],{"class":142,"line":204},11,[140,206,176],{},[10,208,209,210,213,214,217,218,220],{},"MySQL's ",[59,211,212],{},"DATETIME"," type does ",[21,215,216],{},"not"," store timezone information — it is a literal date-time string. Use ",[59,219,67],{}," instead (stored as UTC, converted to session timezone on read), or better yet, store an integer Unix epoch.",[14,222,224],{"id":223},"api-layer","API Layer",[10,226,227],{},"Your API contract should require UTC timestamps in all request and response payloads:",[131,229,233],{"className":230,"code":231,"language":232,"meta":136,"style":136},"language-json shiki shiki-themes github-light github-dark","\u002F\u002F Request: schedule a meeting\n{\n  \"title\": \"Sprint Review\",\n  \"start_time\": \"2026-05-28T14:30:00Z\",\n  \"duration_minutes\": 60\n}\n\n\u002F\u002F Response\n{\n  \"id\": \"evt_abc123\",\n  \"start_time\": \"2026-05-28T14:30:00Z\",\n  \"end_time\": \"2026-05-28T15:30:00Z\"\n}\n","json",[59,234,235,241,247,263,275,285,290,294,299,303,315,325,336],{"__ignoreMap":136},[140,236,237],{"class":142,"line":143},[140,238,240],{"class":239},"sJ8bj","\u002F\u002F Request: schedule a meeting\n",[140,242,243],{"class":142,"line":149},[140,244,246],{"class":245},"sVt8B","{\n",[140,248,249,253,256,260],{"class":142,"line":155},[140,250,252],{"class":251},"sj4cs","  \"title\"",[140,254,255],{"class":245},": ",[140,257,259],{"class":258},"sZZnC","\"Sprint Review\"",[140,261,262],{"class":245},",\n",[140,264,265,268,270,273],{"class":142,"line":161},[140,266,267],{"class":251},"  \"start_time\"",[140,269,255],{"class":245},[140,271,272],{"class":258},"\"2026-05-28T14:30:00Z\"",[140,274,262],{"class":245},[140,276,277,280,282],{"class":142,"line":167},[140,278,279],{"class":251},"  \"duration_minutes\"",[140,281,255],{"class":245},[140,283,284],{"class":251},"60\n",[140,286,287],{"class":142,"line":173},[140,288,289],{"class":245},"}\n",[140,291,292],{"class":142,"line":179},[140,293,183],{"emptyLinePlaceholder":182},[140,295,296],{"class":142,"line":186},[140,297,298],{"class":239},"\u002F\u002F Response\n",[140,300,301],{"class":142,"line":192},[140,302,246],{"class":245},[140,304,305,308,310,313],{"class":142,"line":198},[140,306,307],{"class":251},"  \"id\"",[140,309,255],{"class":245},[140,311,312],{"class":258},"\"evt_abc123\"",[140,314,262],{"class":245},[140,316,317,319,321,323],{"class":142,"line":204},[140,318,267],{"class":251},[140,320,255],{"class":245},[140,322,272],{"class":258},[140,324,262],{"class":245},[140,326,328,331,333],{"class":142,"line":327},12,[140,329,330],{"class":251},"  \"end_time\"",[140,332,255],{"class":245},[140,334,335],{"class":258},"\"2026-05-28T15:30:00Z\"\n",[140,337,339],{"class":142,"line":338},13,[140,340,289],{"class":245},[10,342,343],{},"If clients send local times, your server must convert immediately:",[131,345,349],{"className":346,"code":347,"language":348,"meta":136,"style":136},"language-python shiki shiki-themes github-light github-dark","from datetime import datetime, timezone\n\ndef parse_client_time(iso_string: str, client_tz: str) -> datetime:\n    \"\"\"Parse a local-time ISO string assuming the given timezone, return UTC.\"\"\"\n    from zoneinfo import ZoneInfo\n    tz = ZoneInfo(client_tz)\n    local_dt = datetime.fromisoformat(iso_string).replace(tzinfo=tz)\n    return local_dt.astimezone(timezone.utc)\n","python",[59,350,351,356,360,365,370,375,380,385],{"__ignoreMap":136},[140,352,353],{"class":142,"line":143},[140,354,355],{},"from datetime import datetime, timezone\n",[140,357,358],{"class":142,"line":149},[140,359,183],{"emptyLinePlaceholder":182},[140,361,362],{"class":142,"line":155},[140,363,364],{},"def parse_client_time(iso_string: str, client_tz: str) -> datetime:\n",[140,366,367],{"class":142,"line":161},[140,368,369],{},"    \"\"\"Parse a local-time ISO string assuming the given timezone, return UTC.\"\"\"\n",[140,371,372],{"class":142,"line":167},[140,373,374],{},"    from zoneinfo import ZoneInfo\n",[140,376,377],{"class":142,"line":173},[140,378,379],{},"    tz = ZoneInfo(client_tz)\n",[140,381,382],{"class":142,"line":179},[140,383,384],{},"    local_dt = datetime.fromisoformat(iso_string).replace(tzinfo=tz)\n",[140,386,387],{"class":142,"line":186},[140,388,389],{},"    return local_dt.astimezone(timezone.utc)\n",[14,391,393],{"id":392},"frontend-layer","Frontend Layer",[10,395,396,397,400],{},"The browser's ",[59,398,399],{},"Intl.DateTimeFormat"," API converts UTC to the user's local timezone reliably:",[131,402,406],{"className":403,"code":404,"language":405,"meta":136,"style":136},"language-javascript shiki shiki-themes github-light github-dark","function formatLocalTime(utcISOString) {\n  const date = new Date(utcISOString);\n  return new Intl.DateTimeFormat('en-US', {\n    dateStyle: 'medium',\n    timeStyle: 'short',\n  }).format(date);\n}\n\n\u002F\u002F \"2026-05-28T14:30:00Z\" → \"May 28, 2026, 10:30 AM\" (in US Eastern)\n","javascript",[59,407,408,428,448,469,479,489,500,504,508],{"__ignoreMap":136},[140,409,410,414,418,421,425],{"class":142,"line":143},[140,411,413],{"class":412},"szBVR","function",[140,415,417],{"class":416},"sScJk"," formatLocalTime",[140,419,420],{"class":245},"(",[140,422,424],{"class":423},"s4XuR","utcISOString",[140,426,427],{"class":245},") {\n",[140,429,430,433,436,439,442,445],{"class":142,"line":149},[140,431,432],{"class":412},"  const",[140,434,435],{"class":251}," date",[140,437,438],{"class":412}," =",[140,440,441],{"class":412}," new",[140,443,444],{"class":416}," Date",[140,446,447],{"class":245},"(utcISOString);\n",[140,449,450,453,455,458,461,463,466],{"class":142,"line":155},[140,451,452],{"class":412},"  return",[140,454,441],{"class":412},[140,456,457],{"class":245}," Intl.",[140,459,460],{"class":416},"DateTimeFormat",[140,462,420],{"class":245},[140,464,465],{"class":258},"'en-US'",[140,467,468],{"class":245},", {\n",[140,470,471,474,477],{"class":142,"line":161},[140,472,473],{"class":245},"    dateStyle: ",[140,475,476],{"class":258},"'medium'",[140,478,262],{"class":245},[140,480,481,484,487],{"class":142,"line":167},[140,482,483],{"class":245},"    timeStyle: ",[140,485,486],{"class":258},"'short'",[140,488,262],{"class":245},[140,490,491,494,497],{"class":142,"line":173},[140,492,493],{"class":245},"  }).",[140,495,496],{"class":416},"format",[140,498,499],{"class":245},"(date);\n",[140,501,502],{"class":142,"line":179},[140,503,289],{"class":245},[140,505,506],{"class":142,"line":186},[140,507,183],{"emptyLinePlaceholder":182},[140,509,510],{"class":142,"line":192},[140,511,512],{"class":239},"\u002F\u002F \"2026-05-28T14:30:00Z\" → \"May 28, 2026, 10:30 AM\" (in US Eastern)\n",[10,514,515],{},"Keep your frontend state in UTC. Convert only inside the render function or a computed property. This ensures that any time comparisons, sorting, or arithmetic on the client use a consistent timeline.",[14,517,519],{"id":518},"handling-user-intent","Handling User Intent",[10,521,522],{},"The UTC everywhere pattern handles \"when did this happen?\" perfectly. But \"when should this happen?\" requires extra care when the user's intent is tied to a local time.",[10,524,525,526,529],{},"Consider a daily report scheduled for \"9:00 AM in New York.\" If you store ",[59,527,528],{},"14:00:00Z"," (9 AM EDT), the report will drift to 8:00 AM EST when DST ends in November — because UTC does not change, but the offset does.",[10,531,532,533,536],{},"The correct approach: store the ",[21,534,535],{},"local time and timezone",", not the pre-computed UTC:",[131,538,540],{"className":346,"code":539,"language":348,"meta":136,"style":136},"# Model\nclass ScheduledReport:\n    local_time: time          # 09:00\n    timezone: str             # \"America\u002FNew_York\"\n    days: list[str]           # [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\"]\n\n# Compute next run in UTC at execution time\ndef next_run_utc(report: ScheduledReport) -> datetime:\n    from zoneinfo import ZoneInfo\n    tz = ZoneInfo(report.timezone)\n    now = datetime.now(tz)\n    target = now.replace(hour=report.local_time.hour,\n                         minute=report.local_time.minute,\n                         second=0, microsecond=0)\n    if target \u003C= now:\n        target += timedelta(days=1)\n    return target.astimezone(timezone.utc)\n",[59,541,542,547,552,557,562,567,571,576,581,585,590,595,600,605,611,617,623],{"__ignoreMap":136},[140,543,544],{"class":142,"line":143},[140,545,546],{},"# Model\n",[140,548,549],{"class":142,"line":149},[140,550,551],{},"class ScheduledReport:\n",[140,553,554],{"class":142,"line":155},[140,555,556],{},"    local_time: time          # 09:00\n",[140,558,559],{"class":142,"line":161},[140,560,561],{},"    timezone: str             # \"America\u002FNew_York\"\n",[140,563,564],{"class":142,"line":167},[140,565,566],{},"    days: list[str]           # [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\"]\n",[140,568,569],{"class":142,"line":173},[140,570,183],{"emptyLinePlaceholder":182},[140,572,573],{"class":142,"line":179},[140,574,575],{},"# Compute next run in UTC at execution time\n",[140,577,578],{"class":142,"line":186},[140,579,580],{},"def next_run_utc(report: ScheduledReport) -> datetime:\n",[140,582,583],{"class":142,"line":192},[140,584,374],{},[140,586,587],{"class":142,"line":198},[140,588,589],{},"    tz = ZoneInfo(report.timezone)\n",[140,591,592],{"class":142,"line":204},[140,593,594],{},"    now = datetime.now(tz)\n",[140,596,597],{"class":142,"line":327},[140,598,599],{},"    target = now.replace(hour=report.local_time.hour,\n",[140,601,602],{"class":142,"line":338},[140,603,604],{},"                         minute=report.local_time.minute,\n",[140,606,608],{"class":142,"line":607},14,[140,609,610],{},"                         second=0, microsecond=0)\n",[140,612,614],{"class":142,"line":613},15,[140,615,616],{},"    if target \u003C= now:\n",[140,618,620],{"class":142,"line":619},16,[140,621,622],{},"        target += timedelta(days=1)\n",[140,624,626],{"class":142,"line":625},17,[140,627,628],{},"    return target.astimezone(timezone.utc)\n",[10,630,631],{},"This pattern — store intent in local time, compute execution in UTC — correctly handles DST transitions.",[14,633,635],{"id":634},"key-takeaways","Key Takeaways",[637,638,639,643,651,657,663],"ul",{},[640,641,642],"li",{},"Store, compute, and transmit timestamps in UTC across every layer of your application.",[640,644,91,645,647,648,650],{},[59,646,61],{}," in PostgreSQL and ",[59,649,67],{}," in MySQL; avoid timezone-less types.",[640,652,653,654,656],{},"API contracts should require ",[59,655,79],{}," or explicit offset in all JSON date-time strings.",[640,658,659,660,662],{},"Convert to local time only at the display layer using ",[59,661,399],{}," or equivalent.",[640,664,665],{},"For scheduled events tied to local time, store the local time and timezone, then compute UTC at execution time to handle DST correctly.",[14,667,669],{"id":668},"try-it-yourself","Try It Yourself",[10,671,672,673,678,679,683],{},"Converting between UTC and local timezones? The ",[674,675,677],"a",{"href":676},"\u002Ftools\u002Ftimezone-converter","Timezone Converter"," lets you compare times across any two timezones, and the ",[674,680,682],{"href":681},"\u002Ftools\u002Ftimestamp-converter","Timestamp Converter"," handles Unix-to-ISO conversions with full UTC support.",[685,686,687],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":136,"searchDepth":149,"depth":149,"links":689},[690,691,692,693,694,695,696],{"id":16,"depth":149,"text":17},{"id":122,"depth":149,"text":123},{"id":223,"depth":149,"text":224},{"id":392,"depth":149,"text":393},{"id":518,"depth":149,"text":519},{"id":634,"depth":149,"text":635},{"id":668,"depth":149,"text":669},"2026-05-28","Store UTC, display local — the golden rule for timezone-safe application architecture.","md",{"immutable":182},"\u002Fguides\u002Futc-everywhere-pattern",{"title":5,"description":698},"guides\u002Futc-everywhere-pattern","6V9lRyWJsrfsViOd--X6bTpMGnBFZK2s6KiwQOIranI",1780401338109]