Remote Work Tools

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)")

Built by theluckystrike — More at zovo.one