Remote Employee Time Zone Overlap Optimization Tool for Scheduling Team Meetings
Find optimal meeting times for distributed teams using visualization tools that show time zone overlap, such as World Time Buddy or built-in calendar features in Google Calendar and Outlook. Respecting time zones prevents burnout and shows your team you value work-life balance.
This guide walks through building and using such a tool, with practical code examples you can adapt for your team’s workflow.
The Core Problem
Remote teams typically define “working hours” as something like 9 AM to 6 PM in each person’s local time zone. When you have team members in PST (UTC-8), GMT (UTC+0), and JST (UTC+9), the only overlap in standard working hours is a narrow 2-hour window around 9 AM PST / 5 PM GMT / midnight JST—and that’s already outside normal working hours for Tokyo.
Most scheduling tools simply show you time zones without doing the math to identify overlaps that actually work. That’s where a dedicated overlap optimization tool becomes valuable.
Building a Time Zone Overlap Calculator
Here’s a JavaScript function that calculates overlap windows across multiple time zones:
function findTimeOverlaps(participants, meetingDuration = 60) {
const overlaps = [];
const workStart = 9; // 9 AM
const workEnd = 18; // 6 PM
// Iterate through each 30-minute slot in a 24-hour day
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const slotTime = { hour, minute };
const allInWorkHours = participants.every(p => {
const localHour = convertToLocal(hour, minute, p.timezone);
return localHour >= workStart && localHour < workEnd;
});
if (allInWorkHours) {
overlaps.push({
time: `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`,
participants: participants.map(p => ({
name: p.name,
localTime: convertToLocal(hour, minute, p.timezone)
}))
});
}
}
}
return overlaps;
}
function convertToLocal(utcHour, utcMinute, timezone) {
// Simplified - use a library like luxon for production
const offsets = {
'America/Los_Angeles': -8,
'America/New_York': -5,
'Europe/London': 0,
'Europe/Berlin': 1,
'Asia/Tokyo': 9,
'Asia/Kolkata': 5.5,
'Australia/Sydney': 11
};
const offset = offsets[timezone] || 0;
let localHour = (utcHour + offset + 24) % 24;
return localHour;
}
This basic implementation finds slots where everyone is within working hours. For a more solution, use the luxon or date-fns-tz libraries which handle daylight saving time transitions correctly.
Practical Tool Options
Several existing tools solve this problem without building from scratch:
World Time Buddy provides a visual timeline where you can drag participants across time zones and see overlap regions highlighted in green. It’s particularly useful for one-off scheduling but less ideal for recurring meetings.
When2meet creates a heatmap visualization showing availability across a group, with darker colors indicating more people available. Teams often use this before establishing regular meeting schedules.
Slack’s Built-in Time Zone Support works if everyone sets their time zone in their profile. While it doesn’t calculate overlaps automatically, you can reference it when proposing times in Slack threads.
Custom Slack Integration offers the most power. You can build a simple Slack command that accepts participant names and returns available slots:
# Slack command handler example (Python/Flask)
@app.route('/slack/overlap', methods=['POST'])
def calculate_overlap():
user_ids = request.form['text'].split()
team = get_team_from_slack(user_ids)
overlaps = find_time_overlaps(
participants=[get_user_tz(uid) for uid in user_ids],
meeting_duration=60
)
response = "Available meeting slots:\n"
for slot in overlaps[:5]:
response += f"• {slot['utc']} UTC - works for all\n"
return Response(response, mimetype='text/plain')
Implementing Weighted Preferences
Not all team members have equal scheduling priority. Senior engineers in critical time zones might warrant more flexibility, while contractors might have narrower windows. A weighted system handles this:
function findWeightedOverlaps(participants, weights) {
const slotScores = {};
for (let hour = 0; hour < 24; hour++) {
let score = 0;
for (const p of participants) {
const localHour = convertToLocal(hour, 0, p.timezone);
const ideal = localHour >= 10 && localHour <= 16; // Core hours
const acceptable = localHour >= 9 && localHour < 18;
if (ideal) score += weights[p.name] * 2;
else if (acceptable) score += weights[p.name];
else score -= weights[p.name] * 2; // Penalize outside hours
}
slotScores[hour] = score;
}
return Object.entries(slotScores)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
}
This scores each hour based on how well it works for each participant, then ranks slots by total score. You can then present the top 3-5 options to the team.
Automation Strategies
For recurring meetings, automate the selection process entirely. Create a scheduled job that runs weekly, identifies the best slots, and proposes them in your team channel:
// Weekly meeting slot proposal
async function proposeWeeklyMeeting() {
const team = await getTeamData();
const overlaps = findWeightedOverlaps(team.members, team.weights);
const proposal = overlaps.map(([hour, score], index) =>
`${index + 1}. ${hour}:00 UTC (score: ${score})`
).join('\n');
await postToSlack('#meetings',
`📅 Weekly sync proposals for next week:\n${proposal}\nReact with ✅ to confirm`);
}
This approach removes the negotiation overhead entirely. Team members just confirm or request adjustments.
Handling Edge Cases
International teams must account for several complications:
Daylight Saving Time: Always use IANA time zone identifiers (like “America/New_York”) rather than fixed offsets. Libraries like Luxon handle DST transitions automatically.
Flexible Hours: Some team members work non-standard schedules. Allow participants to specify their actual availability rather than assuming 9-6.
One-Time vs Recurring: A tool should distinguish between finding a single slot (more flexibility) and establishing a recurring meeting (needs long-term stability).
Public Holidays: For monthly or quarterly planning, factor in regional holidays that affect availability in specific time zones.
Tool Comparison Table
| Tool | Best For | Price | Integrations | Setup Time |
|---|---|---|---|---|
| World Time Buddy | One-off scheduling | Free tier, $29/yr | Calendar APIs | 2 min |
| When2meet | Group availability | Free | Email invites | 5 min |
| Calendly | Recurring meetings | $10-16/mo | Slack, Teams, Zapier | 10 min |
| Slack Workflow | In-channel scheduling | Built-in | Slack only | 15 min |
| Custom Node.js | Maximum control | Free | Any webhook | 1-2 hours |
| Google Calendar | Built-in timezone | Free | Gmail, Meet | Already set up |
| Outlook Calendar | Enterprise deployments | Included | Teams, Exchange | Already set up |
Production Implementation with Timezone Holidays
For distributed teams spanning multiple countries, integrate a holiday calendar API:
// Production-ready timezone overlap with holiday awareness
const axios = require('axios');
const { zonedTimeToUtc, utcToZonedTime } = require('date-fns-tz');
async function findAvailableSlots(team, targetDate, excludeHolidays = true) {
const availableSlots = [];
// Fetch holiday data for all team timezones
const holidays = excludeHolidays ?
await fetchHolidaysForTeam(team, targetDate) : {};
// Check each hour in the target date
for (let utcHour = 0; utcHour < 24; utcHour++) {
const slotTime = new Date(targetDate);
slotTime.setUTCHours(utcHour, 0, 0, 0);
// Check if any team member has a holiday on this date
const isHoliday = team.some(member =>
holidays[member.timezone]?.includes(formatDate(slotTime))
);
if (isHoliday) continue;
const localTimes = team.map(member => {
const zoned = utcToZonedTime(slotTime, member.timezone);
return {
name: member.name,
localHour: zoned.getHours(),
timezone: member.timezone
};
});
const allInWorkHours = localTimes.every(t =>
t.localHour >= 9 && t.localHour < 18
);
if (allInWorkHours) {
availableSlots.push({
utcTime: slotTime,
localTimes: localTimes,
quality: calculateSlotQuality(localTimes)
});
}
}
return availableSlots.sort((a, b) => b.quality - a.quality);
}
async function fetchHolidaysForTeam(team, targetDate) {
// Use Calendarific API (paid) or holiday-jp, node-holiday, etc.
const holidays = {};
for (const member of team) {
const countryCode = getCountryFromTimezone(member.timezone);
const response = await axios.get(
`https://calendarific.com/api/v2/holidays?api_key=${process.env.CALENDARIFIC_KEY}&country=${countryCode}&year=${targetDate.getFullYear()}`
);
holidays[member.timezone] = response.data.response.holidays.map(h => h.date);
}
return holidays;
}
function calculateSlotQuality(localTimes) {
// Prefer slots where everyone is in core hours (10 AM - 4 PM)
const coreHourBonus = localTimes.filter(t =>
t.localHour >= 10 && t.localHour <= 16
).length;
// Penalize very early or very late slots
const offPeakPenalty = localTimes.filter(t =>
t.localHour < 8 || t.localHour > 19
).length * 0.5;
return coreHourBonus - offPeakPenalty;
}
Integration with Existing Tools
Slack Bot Implementation
Use a Slack bot to propose meeting times directly in your team channel:
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
app.command('/suggest-meeting', async ({ ack, body, client }) => {
ack();
const userIds = body.text.split(' ');
const team = await Promise.all(
userIds.map(id => getUserTimezone(client, id))
);
const slots = await findAvailableSlots(team, new Date());
const blocks = [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Meeting Time Suggestions for ${team.map(t => t.name).join(', ')}*`
}
}
];
slots.slice(0, 5).forEach((slot, idx) => {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: `*Option ${idx + 1}:* ${slot.utcTime.toISOString()}\n${slot.localTimes.map(t =>
`${t.name}: ${formatHour(t.localHour)}`
).join(' | ')}`
},
accessory: {
type: 'button',
text: { type: 'plain_text', text: 'Confirm' },
value: slot.utcTime.toISOString()
}
});
});
await client.chat.postMessage({
channel: body.channel_id,
blocks: blocks
});
});
async function getUserTimezone(client, userId) {
const user = await client.users.info({ user: userId });
return {
name: user.user.real_name,
timezone: user.user.tz || 'UTC',
userId: userId
};
}
Google Calendar Event Creation
After confirming a meeting time, automatically create calendar events for all participants:
const google = require('googleapis').google;
async function createCalendarEvent(team, slotTime) {
const calendar = google.calendar('v3');
const events = [];
for (const member of team) {
const auth = getAuthForUser(member.userId);
const zoned = utcToZonedTime(slotTime, member.timezone);
const endTime = new Date(zoned);
endTime.setHours(endTime.getHours() + 1);
const event = {
summary: 'Team Meeting - Timezone Optimized',
description: `Meeting scheduled using timezone overlap optimization tool.\nYour local time: ${formatTime(zoned)}`,
start: {
dateTime: zoned.toISOString(),
timeZone: member.timezone
},
end: {
dateTime: endTime.toISOString(),
timeZone: member.timezone
},
attendees: team.map(t => ({ email: t.email })),
reminders: {
useDefault: false,
overrides: [
{ method: 'notification', minutes: 24 * 60 },
{ method: 'notification', minutes: 15 }
]
}
};
const response = await calendar.events.insert({
auth: auth,
calendarId: 'primary',
resource: event
});
events.push(response.data);
}
return events;
}
Handling Recurring Meetings Across DST Changes
Daylight Saving Time transitions create scheduling chaos. This function finds recurring slots that remain stable year-round:
function findStableRecurringSlot(team, idealDayOfWeek = 2) {
// Monday = 0, Sunday = 6
const targetDay = new Date();
targetDay.setDate(targetDay.getDate() + (idealDayOfWeek - targetDay.getDay()));
const slotCandidates = [];
// Test every hour for the next 12 months
for (let hour = 0; hour < 24; hour++) {
let isStable = true;
const testDates = [];
// Sample across DST boundaries
for (let month of [0, 2, 5, 10]) {
const testDate = new Date(targetDay.getFullYear(), month, targetDay.getDate());
testDate.setUTCHours(hour, 0, 0, 0);
testDates.push(testDate);
}
// Check if this hour works for everyone in all DST scenarios
for (const testDate of testDates) {
const localTimes = team.map(member => {
const zoned = utcToZonedTime(testDate, member.timezone);
return zoned.getHours();
});
const allInWorkHours = localTimes.every(h => h >= 9 && h < 18);
if (!allInWorkHours) {
isStable = false;
break;
}
}
if (isStable) {
slotCandidates.push({
utcHour: hour,
stability: 'year_round',
dstSafe: true
});
}
}
return slotCandidates;
}
Monitoring and Adjustment
Track how well your meeting schedule works and auto-adjust quarterly:
# Python version for analytics tracking
import json
from datetime import datetime, timedelta
from dataclasses import dataclass
@dataclass
class MeetingFeedback:
meeting_id: str
timestamp: datetime
participants: list
feedback_scores: dict # 'early': -2, 'late': -2, 'perfect': 1
notes: str
class SchedulingAnalytics:
def __init__(self):
self.feedback = []
def log_feedback(self, meeting_id, participants, scores, notes=""):
feedback = MeetingFeedback(
meeting_id=meeting_id,
timestamp=datetime.now(),
participants=participants,
feedback_scores=scores,
notes=notes
)
self.feedback.append(feedback)
self.save()
def calculate_optimal_time(self, quarters=4):
# Analyze last N quarters of meetings
recent_feedback = self.feedback[-quarters*12:] # Last ~12 months
hour_scores = {}
for entry in recent_feedback:
for hour in range(24):
if hour not in hour_scores:
hour_scores[hour] = {'score': 0, 'count': 0}
# Extract average feedback for meetings at this hour
avg_score = sum(entry.feedback_scores.values()) / len(entry.feedback_scores)
hour_scores[hour]['score'] += avg_score
hour_scores[hour]['count'] += 1
# Find the hour with the best average feedback
best_hour = max(
hour_scores.items(),
key=lambda x: x[1]['score'] / x[1]['count'] if x[1]['count'] > 0 else 0
)
return {
'optimal_utc_hour': best_hour[0],
'average_satisfaction': best_hour[1]['score'] / best_hour[1]['count'],
'sample_size': best_hour[1]['count']
}
def save(self):
with open('meeting_analytics.json', 'w') as f:
json.dump([
{
'meeting_id': fb.meeting_id,
'timestamp': fb.timestamp.isoformat(),
'participants': fb.participants,
'feedback_scores': fb.feedback_scores,
'notes': fb.notes
}
for fb in self.feedback
], f, indent=2)
# Usage
analytics = SchedulingAnalytics()
analytics.log_feedback(
meeting_id='team-standup-2026-03-21',
participants=['alice@example.com', 'bob@example.com', 'charlie@example.com'],
scores={'alice': 1, 'bob': 0, 'charlie': -1},
notes='Early for Tokyo, late for Los Angeles'
)
optimal = analytics.calculate_optimal_time(quarters=2)
print(f"Move meeting to UTC {optimal['optimal_utc_hour']} ({optimal['average_satisfaction']:.1f}/1.0 satisfaction)")
Related Articles
- Remote Employee Time Zone Overlap Optimization Tool
- Remote Work Time Zone Overlap Calculator Tools 2026
- Team hours (as datetime.time objects converted to hours)
- Example: Create a booking via API
- Best Time Zone Management Tools for Distributed Engineering
Built by theluckystrike — More at zovo.one