Your competitor posts 4 times a week. Their engagement is consistently 3x yours.
Is it the content? Maybe. But what if it's when they post?
Timing matters more than most developers think. The algorithm doesn't just evaluate content quality — it evaluates initial engagement velocity. Post when your audience is online, and you get that early boost. Post when they're asleep, and your content is buried before anyone sees it.
I built a tool that analyzes any creator's posting history and extracts their exact schedule — day of week, hour of day, and which time slots get the best results. Here's the whole thing.
The Stack
- Node.js – runtime
- SociaVault API – fetch post history with timestamps
- Statistics – frequency analysis, heatmap generation
Step 1: Fetch Post History
const axios = require('axios');
const API_BASE = 'https://api.sociavault.com/v1';
const API_KEY = process.env.SOCIAVAULT_API_KEY;
const api = axios.create({
baseURL: API_BASE,
headers: { 'x-api-key': API_KEY },
});
async function getPostHistory(platform, username, limit = 100) {
const { data } = await api.get(`/${platform}/posts/${username}`, {
params: { limit },
});
return data.posts;
}
async function getProfile(platform, username) {
const { data } = await api.get(`/${platform}/profile/${username}`);
return data;
}
Step 2: Extract the Schedule
Convert raw post timestamps into a day-of-week × hour-of-day frequency map.
function extractPostingSchedule(posts, timezone = 'America/New_York') {
const dayHourMap = {};
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
// Initialize 7×24 grid
for (let day = 0; day < 7; day++) {
dayHourMap[dayNames[day]] = {};
for (let hour = 0; hour < 24; hour++) {
dayHourMap[dayNames[day]][hour] = { count: 0, totalLikes: 0, totalComments: 0, posts: [] };
}
}
for (const post of posts) {
const date = new Date(post.createdAt);
// Convert to target timezone
const localTime = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
const day = dayNames[localTime.getDay()];
const hour = localTime.getHours();
dayHourMap[day][hour].count++;
dayHourMap[day][hour].totalLikes += post.likeCount || 0;
dayHourMap[day][hour].totalComments += post.commentCount || 0;
dayHourMap[day][hour].posts.push(post);
}
return dayHourMap;
}
Step 3: Find Peak Posting Times
Which time slots does the competitor use most? And which perform best?
function findPeakTimes(dayHourMap) {
const slots = [];
for (const [day, hours] of Object.entries(dayHourMap)) {
for (const [hour, data] of Object.entries(hours)) {
if (data.count === 0) continue;
const avgLikes = Math.round(data.totalLikes / data.count);
const avgComments = Math.round(data.totalComments / data.count);
slots.push({
day,
hour: parseInt(hour),
hourFormatted: formatHour(parseInt(hour)),
postCount: data.count,
avgLikes,
avgComments,
});
}
}
return {
byFrequency: [...slots].sort((a, b) => b.postCount - a.postCount),
byPerformance: [...slots].sort((a, b) => b.avgLikes - a.avgLikes),
};
}
function formatHour(hour) {
if (hour === 0) return '12 AM';
if (hour === 12) return '12 PM';
if (hour < 12) return `${hour} AM`;
return `${hour - 12} PM`;
}
Step 4: Generate a Posting Heatmap
A visual heatmap shows the entire schedule at a glance.
function generateHeatmap(dayHourMap) {
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const hours = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]; // 6 AM - 10 PM
// Find max count for normalization
let maxCount = 0;
for (const day of days) {
for (const hour of hours) {
if (dayHourMap[day][hour].count > maxCount) {
maxCount = dayHourMap[day][hour].count;
}
}
}
console.log('\n📅 POSTING FREQUENCY HEATMAP');
console.log(' (░ = never, ▒ = sometimes, ▓ = often, █ = peak)\n');
// Header
console.log(' ' + hours.map(h => formatHour(h).padStart(5)).join(' '));
for (const day of days) {
let row = day.padEnd(10);
for (const hour of hours) {
const count = dayHourMap[day][hour].count;
const intensity = maxCount > 0 ? count / maxCount : 0;
let block;
if (intensity === 0) block = ' ░ ';
else if (intensity < 0.33) block = ' ▒ ';
else if (intensity < 0.66) block = ' ▓ ';
else block = ' █ ';
row += block;
}
console.log(row);
}
}
Step 5: Compare Your Schedule vs Competitor's
function compareSchedules(mySchedule, compSchedule) {
const myPeak = findPeakTimes(mySchedule);
const compPeak = findPeakTimes(compSchedule);
// Slots competitor uses heavily that you don't
const gaps = [];
const compTopSlots = compPeak.byPerformance.slice(0, 10);
for (const slot of compTopSlots) {
const mySlotData = mySchedule[slot.day]?.[slot.hour];
const myCount = mySlotData?.count || 0;
if (myCount === 0) {
gaps.push({
...slot,
type: 'untapped',
message: `Competitor posts ${slot.day} at ${slot.hourFormatted} (avg ${slot.avgLikes.toLocaleString()} likes) — you never post here`,
});
} else if (myCount < slot.postCount * 0.5) {
gaps.push({
...slot,
type: 'underused',
message: `Competitor posts ${slot.day} at ${slot.hourFormatted} ${slot.postCount}x — you only post ${myCount}x`,
});
}
}
return gaps;
}
Step 6: Find Optimal Posting Windows
Combine frequency + performance to find the optimal schedule.
function findOptimalSchedule(dayHourMap, slotsPerWeek = 5) {
const scored = [];
for (const [day, hours] of Object.entries(dayHourMap)) {
for (const [hour, data] of Object.entries(hours)) {
if (data.count === 0) continue;
// Score = avgLikes * consistency bonus
const avgLikes = data.totalLikes / data.count;
const consistencyBonus = Math.min(data.count / 3, 2); // Bonus for slots used 3+ times
scored.push({
day,
hour: parseInt(hour),
hourFormatted: formatHour(parseInt(hour)),
score: avgLikes * consistencyBonus,
avgLikes: Math.round(avgLikes),
timesUsed: data.count,
});
}
}
// Pick top N non-overlapping slots
const optimal = scored
.sort((a, b) => b.score - a.score)
.slice(0, slotsPerWeek);
return optimal;
}
Putting It All Together
async function analyzeCompetitorSchedule(platform, competitorUsername, myUsername) {
console.log(`\n=== POSTING SCHEDULE ANALYSIS ===`);
console.log(`Competitor: @${competitorUsername}`);
console.log(`Your account: @${myUsername}\n`);
// Fetch post history for both
const compPosts = await getPostHistory(platform, competitorUsername, 100);
const myPosts = await getPostHistory(platform, myUsername, 100);
const compProfile = await getProfile(platform, competitorUsername);
const myProfile = await getProfile(platform, myUsername);
console.log(`Competitor: ${compPosts.length} posts analyzed (${compProfile.followerCount.toLocaleString()} followers)`);
console.log(`You: ${myPosts.length} posts analyzed (${myProfile.followerCount.toLocaleString()} followers)`);
// Extract schedules
const compSchedule = extractPostingSchedule(compPosts);
const mySchedule = extractPostingSchedule(myPosts);
// Competitor heatmap
console.log(`\n@${competitorUsername}'s posting pattern:`);
generateHeatmap(compSchedule);
// Peak times
const compPeaks = findPeakTimes(compSchedule);
console.log('\n🏆 Competitor\'s Top 5 Time Slots (by performance):');
compPeaks.byPerformance.slice(0, 5).forEach((slot, i) => {
console.log(
` ${i + 1}. ${slot.day} ${slot.hourFormatted} — ` +
`avg ${slot.avgLikes.toLocaleString()} likes, ` +
`used ${slot.postCount}x`
);
});
// Schedule gaps
const gaps = compareSchedules(mySchedule, compSchedule);
if (gaps.length > 0) {
console.log('\n⚡ Schedule Gaps (competitor uses, you don\'t):');
gaps.forEach((gap, i) => {
console.log(` ${i + 1}. ${gap.message}`);
});
}
// Optimal schedule recommendation
const optimal = findOptimalSchedule(compSchedule, 5);
console.log('\n📋 Recommended Posting Schedule (based on competitor data):');
optimal.forEach((slot, i) => {
console.log(` ${i + 1}. ${slot.day} at ${slot.hourFormatted} (expected ~${slot.avgLikes.toLocaleString()} likes)`);
});
}
analyzeCompetitorSchedule('tiktok', 'competitor_handle', 'my_handle');
Sample Output
=== POSTING SCHEDULE ANALYSIS ===
Competitor: @fitnessguru_mike
Your account: @my_fitness_page
Competitor: 94 posts analyzed (245,000 followers)
You: 78 posts analyzed (52,000 followers)
@fitnessguru_mike's posting pattern:
📅 POSTING FREQUENCY HEATMAP
(░ = never, ▒ = sometimes, ▓ = often, █ = peak)
6 AM 7 AM 8 AM 9 AM 10 AM 11 AM 12 PM ...
Monday ░ ░ ▓ █ ▒ ░ ░
Tuesday ░ ░ ▒ ▒ ░ ░ ▓
Wednesday ░ ░ ░ █ ▓ ░ ░
Thursday ░ ░ ▓ ▒ ░ ░ ▒
Friday ░ ░ ░ ░ ░ ░ ░
Saturday ░ ▒ ▓ ▓ ░ ░ ░
Sunday ░ ░ ░ ▒ ░ ░ ░
🏆 Competitor's Top 5 Time Slots (by performance):
1. Monday 9 AM — avg 18,400 likes, used 8x
2. Wednesday 9 AM — avg 16,200 likes, used 7x
3. Saturday 8 AM — avg 14,800 likes, used 5x
4. Tuesday 12 PM — avg 13,100 likes, used 4x
5. Thursday 8 AM — avg 11,900 likes, used 6x
⚡ Schedule Gaps (competitor uses, you don't):
1. Competitor posts Saturday at 8 AM (avg 14,800 likes) — you never post here
2. Competitor posts Wednesday at 10 AM (avg 12,400 likes) — you never post here
📋 Recommended Posting Schedule (based on competitor data):
1. Monday at 9 AM (expected ~18,400 likes)
2. Wednesday at 9 AM (expected ~16,200 likes)
3. Saturday at 8 AM (expected ~14,800 likes)
4. Tuesday at 12 PM (expected ~13,100 likes)
5. Thursday at 8 AM (expected ~11,900 likes)
They post at 9 AM on Monday and Wednesday — and those are their highest performing slots. You're posting at 3 PM on random days. That alone could explain the 3x engagement gap.
Read the Full Guide
This is a condensed version. The full guide includes:
- Timezone detection from audience location data
- Multi-competitor schedule overlap analysis
- Automated scheduling integration (Buffer, Later API)
- Seasonal and trend-adjusted timing
Read the complete guide on SociaVault →
Building social media analytics tools? SociaVault provides social media data APIs for TikTok, Instagram, YouTube, and 10+ platforms. Fetch post history with timestamps, engagement data, and creator profiles through one unified API.
Discussion
When do you post? Is it strategic or "whenever I finish editing"? Have you ever tested different times and seen a real difference? 👇
Top comments (0)