- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Timezones: The Bug That Waits Patiently for Daylight Saving Time
About Post
Timezone bugs are patient. They don't show up in code review, they don't fail your tests, and they don't appear when you deploy. They wait, sometimes for months, for the one night the clocks change. Then a reminder goes out an hour late, a report drops a payment from "today", or a scheduled job runs twice.
I work from Doha, where there's no daylight saving time at all. That makes it very easy to forget that much of the world moves its clocks twice a year. Europe and the US went back an hour only a few weeks ago, and somewhere a bug that slept peacefully since March just woke up.
Here are the mistakes that cause most of them, why each one bites, and the fix.
Mistake 1: storing local time
The mistake: saving timestamps in the server's time, or the user's, without saying which. Then the server moves region, a second office opens, or DST kicks in, and half your data means something different from the other half.
Why it bites: a local time like 2025-10-26 01:30 in London happened twice this year, once before the clocks went back and once after. Without the timezone, you can't tell which.
The fix: store everything in UTC. Keep the app timezone set to UTC in config/app.php and make sure the database connection doesn't quietly convert values. UTC has no daylight saving, no ambiguity and no surprises.
Mistake 2: converting in the wrong place
The rule: UTC everywhere inside the system, local time only at the edges, when data comes in from a person and when it goes out to one.
// In: the user typed a local time
$startsAt = Carbon::parse($request->input('starts_at'), $user->timezone)->utc();
// Out: show it in the viewer's timezone
$booking->starts_at->setTimezone($viewer->timezone)->format('d M Y, H:i');
Notice it's the viewer's timezone on the way out, which isn't always the creator's. A maintenance visit booked by staff in Doha and viewed by an owner in London should show each of them their own clock.
Mistake 3: storing an offset instead of a timezone
The mistake: saving the user's timezone as +00:00 or +01:00.
Why it bites: an offset is a snapshot, not a place. London is +00:00 in winter and +01:00 in summer. Store the offset in July and every winter date you calculate with it is an hour off.
The fix: store the IANA timezone name, like Europe/London, Asia/Qatar or America/New_York. PHP and Carbon know the rules for each, including when they change.
Mistake 4: "a day" is not always 24 hours
On the day the clocks go forward, a local day has 23 hours. On the day they go back, it has 25. That breaks any code that treats "tomorrow" as "plus 86,400 seconds".
$t = Carbon::parse('2025-03-29 09:00', 'Europe/London');
$t->copy()->addDay(); // 2025-03-30 09:00 BST: same wall-clock time
$t->copy()->addHours(24); // 2025-03-30 10:00 BST: 24 real hours later
Both are correct; they answer different questions. "Remind me at the same time tomorrow" wants addDay(). "Expire this link in 24 hours" wants addHours(24). Decide which one you mean.
Mistake 5: "today" in the wrong timezone
A dashboard shows "payments received today". The query uses whereDate('paid_at', today()), and today() is a UTC day. For a team three hours ahead of UTC, the first three hours of their morning belong to "yesterday".
The fix: work out the day's boundaries in the user's timezone, then convert them to UTC for the query:
$tz = $user->timezone; // e.g. 'Asia/Qatar'
$start = now($tz)->startOfDay()->utc();
$end = now($tz)->endOfDay()->utc();
$total = Payment::whereBetween('paid_at', [$start, $end])->sum('amount');
As a bonus, this keeps the query index-friendly, unlike wrapping the column in a date function.
Mistake 6: scheduling in local time
Laravel's scheduler lets you run a task in a specific timezone, like ->dailyAt('01:30')->timezone('Europe/London'). It's handy, but think about what happens at the transitions: in spring, UK clocks jump from 01:00 to 02:00, so 01:30 never happens that night; in autumn, 01:30 happens twice. The Laravel docs themselves recommend avoiding timezone scheduling when you can for exactly this reason.
The fix: schedule system jobs in UTC. For per-user schedules ("send each tenant a reminder at 9 AM their time"), run a job every few minutes or every hour in UTC that finds the users whose local time has just passed 9 AM and who haven't been sent today's reminder yet. Store the "already sent" fact so a repeated hour can't send twice.
Mistake 7: treating dates as timestamps
Some values aren't moments in time. A birthday, a contract start date, a rent due date: these are calendar dates, and they should be the same date everywhere in the world.
Why it bites: store 2025-12-01 as midnight UTC, convert it to a timezone behind UTC, and it becomes 30 November. JavaScript does this to you for free: new Date('2025-12-01') is parsed as midnight UTC, so in the Americas it displays as the previous day.
The fix: use a DATE column, send it in your API as a plain "2025-12-01" string, never convert it between timezones, and be careful about passing it to new Date() on the front end.
Three rules that prevent most timezone bugs: store moments in UTC, store places as IANA timezone names, and keep calendar dates as plain dates. Convert only when talking to humans.
Test the dangerous days on purpose
Don't wait for March. Freeze time at the transitions and see what your code does (a simplified test; ReminderScheduler stands in for your own class):
public function test_reminder_time_survives_the_clock_change(): void
{
$this->travelTo(Carbon::parse('2025-10-25 09:00', 'Europe/London'));
$next = app(ReminderScheduler::class)->nextRunFor($this->londonUser);
$this->assertSame('2025-10-26 09:00',
$next->setTimezone('Europe/London')->format('Y-m-d H:i'));
}
Write a few tests like this around the spring and autumn transitions in the timezones your users live in. They're cheap, and they catch the bugs that otherwise only appear twice a year.
What's the strangest timezone bug you've chased? Mine always seem to involve a report that's "off by one" only for some users.

Be first to comment it...