PostPost

Rate Limits

This guide covers platform-specific rate limits, PostPost API rate limits, and strategies for scheduling posts at optimal engagement times.

Quick Start: High-Volume Multi-Platform Scheduling

For scheduling hundreds of posts across all 10 platforms simultaneously, use this production-ready approach:

Installation

# JavaScript/Node.js
npm install axios

# Python
pip install requests

Environment Setup

# Set your API key (get it from postpost.dev → API in sidebar)
export PUBLORA_API_KEY="sk_your_api_key_here"

Minimal Working Example

JavaScript (Node.js):

// high-volume-scheduler.js
// Run: PUBLORA_API_KEY=sk_xxx node high-volume-scheduler.js

const axios = require('axios');

const API_KEY = process.env.PUBLORA_API_KEY;
const BASE_URL = 'https://api.postpost.dev/api/v1';

// Advisory/recommended limits — NOT enforced by PostPost.
// These are conservative guidelines to avoid platform-side rate limiting.
const PLATFORM_LIMITS = {
  twitter:   { perHour: 10, perDay: 50 },   // advisory
  linkedin:  { perHour: 5,  perDay: 20 },   // advisory
  instagram: { perHour: 3,  perDay: 10 },   // advisory
  threads:   { perHour: 10, perDay: 50 },   // advisory
  tiktok:    { perHour: 2,  perDay: 10 },   // advisory
  facebook:  { perHour: 5,  perDay: 25 },   // advisory
  youtube:   { perHour: 2,  perDay: 10 },   // advisory
  bluesky:   { perHour: 10, perDay: 100 },  // advisory
  mastodon:  { perHour: 5,  perDay: 50 },   // advisory
  telegram:  { perHour: 20, perDay: 300 },  // advisory
};

async function schedulePost(content, platforms, scheduledTime) {
  const response = await axios.post(`${BASE_URL}/create-post`, {
    content,
    platforms,
    scheduledTime
  }, {
    headers: {
      'x-api-key': API_KEY,
      'Content-Type': 'application/json'
    }
  });
  return response.data;
}

async function main() {
  // Your platform connection IDs (from /platform-connections endpoint)
  const platforms = ['twitter-123456', 'linkedin-ABC123', 'threads-789012'];

  // Schedule 10 posts, 1 hour apart
  const now = new Date();
  for (let i = 0; i < 10; i++) {
    const scheduledTime = new Date(now.getTime() + (i + 1) * 3600000);
    const result = await schedulePost(
      `Post ${i + 1}: Your content here`,
      platforms,
      scheduledTime.toISOString()
    );
    console.log(`Scheduled post ${i + 1}: ${result.postGroupId}`);
  }
}

main().catch(console.error);

Python:

#!/usr/bin/env python3
# high_volume_scheduler.py
# Run: PUBLORA_API_KEY=sk_xxx python high_volume_scheduler.py

import os
import requests
from datetime import datetime, timedelta, timezone

API_KEY = os.environ['PUBLORA_API_KEY']
BASE_URL = 'https://api.postpost.dev/api/v1'

# Advisory/recommended limits — NOT enforced by PostPost.
# These are conservative guidelines to avoid platform-side rate limiting.
PLATFORM_LIMITS = {
    'twitter':   {'per_hour': 10, 'per_day': 50},    # advisory
    'linkedin':  {'per_hour': 5,  'per_day': 20},    # advisory
    'instagram': {'per_hour': 3,  'per_day': 10},    # advisory
    'threads':   {'per_hour': 10, 'per_day': 50},    # advisory
    'tiktok':    {'per_hour': 2,  'per_day': 10},    # advisory
    'facebook':  {'per_hour': 5,  'per_day': 25},    # advisory
    'youtube':   {'per_hour': 2,  'per_day': 10},    # advisory
    'bluesky':   {'per_hour': 10, 'per_day': 100},   # advisory
    'mastodon':  {'per_hour': 5,  'per_day': 50},    # advisory
    'telegram':  {'per_hour': 20, 'per_day': 300},   # advisory
}

def schedule_post(content: str, platforms: list, scheduled_time: str) -> dict:
    response = requests.post(
        f'{BASE_URL}/create-post',
        json={
            'content': content,
            'platforms': platforms,
            'scheduledTime': scheduled_time
        },
        headers={
            'x-api-key': API_KEY,
            'Content-Type': 'application/json'
        }
    )
    response.raise_for_status()
    return response.json()

def main():
    # Your platform connection IDs (from /platform-connections endpoint)
    platforms = ['twitter-123456', 'linkedin-ABC123', 'threads-789012']

    # Schedule 10 posts, 1 hour apart
    now = datetime.now(timezone.utc)
    for i in range(10):
        scheduled_time = now + timedelta(hours=i + 1)
        result = schedule_post(
            f'Post {i + 1}: Your content here',
            platforms,
            scheduled_time.isoformat()
        )
        print(f"Scheduled post {i + 1}: {result['postGroupId']}")

if __name__ == '__main__':
    main()

PostPost Plan Limits

Each PostPost plan enforces limits on monthly posts, scheduled (pending) posts, and how far in advance you can schedule.

PlanMonthly PostsConnectionsPost Limit ScopeScheduled PostsSchedule HorizonAPI/MCP AccessPlatforms
Starter (Free)153Account-wide3 pending7 daysNoAll except Twitter
Pro Monthly ($5.99/mo)100Per seat*Per connection100 pendingUnlimitedYesAll
Pro Yearly ($2.99/mo)100Per seat*Per connection100 pendingUnlimitedYesAll
Premium Monthly ($9.99/mo)500Per seat*Per connection500 pendingUnlimitedYesAll
Premium Yearly ($5.99/mo)500Per seat*Per connection500 pendingUnlimitedYesAll

*Paid plan connection limits are based on subscription quantity (seat count), not truly unlimited. Each seat in your subscription adds to your total allowed connections.

Key details:

  • Starter plan limits are account-wide (15 posts total across all connections, max 3 connections, scheduledPosts: 3, scheduleHorizonDays: 7). Paid plans count limits per connection (e.g., 100 posts per LinkedIn connection + 100 posts per Twitter connection).
  • Starter plan does not include API or MCP access (apiAccess: false, mcpAccess: false). You must upgrade to Pro or Premium to use the REST API or MCP server. Starter is dashboard-only.
  • Starter plan excludes Twitter/X. To post to Twitter, upgrade to a paid plan.

Note: Per-request API rate limiting (requests/minute) is planned but not currently enforced. The limits above (monthly posts, scheduled posts, schedule horizon) are the enforced plan-based limits. Future API rate limiting will be documented here when implemented.

Platform-Specific Rate Limits (Advisory)

Each social media platform has its own posting limits. The table below shows advisory/recommended limits based on platform documentation and best practices. These limits are not enforced by PostPost -- they are guidelines to help you avoid being rate-limited or penalized by the platforms themselves. PostPost handles automatic queuing and retry when platform limits are hit.

All 10 Supported Platforms

PlatformPosts/Day (platform limit)Posts/Hour (platform limit)PostPost RecommendedNotes
X/Twitter2,400 tweets300 tweets50/day, 10/hourThreading counts as multiple posts
LinkedIn100 posts25 posts20/day, 5/hourPersonal + organization pages
Instagram25 posts10 posts10/day, 3/hourFeed posts only; Stories separate
Threads250 posts50 posts50/day, 10/hourThreading supported
TikTok50 videos10 videos10/day, 2/hourVideo uploads only
YouTube50 videos10 videos10/day, 2/hourPer channel
Facebook50 posts25 posts25/day, 5/hourPer page
Bluesky1,666 posts100 posts100/day, 10/hourPer account
Mastodon300 posts30 posts50/day, 5/hourInstance-dependent
TelegramUnlimited20 msg/sec300/day, 20/hourChannel/group dependent

"PostPost Recommended" limits are conservative defaults built into the scheduler examples below to ensure reliable publishing without hitting platform limits.

How PostPost Handles Platform Limits

  1. Automatic Queuing - If a platform rate limit is hit, PostPost queues the post and retries automatically
  2. Smart Distribution - When scheduling many posts, PostPost distributes them to avoid hitting limits
  3. Error Reporting - If a post fails due to platform limits, status shows failed with the reason

API Reference for Scheduling

The scheduler examples use these PostPost API endpoints:

Create Post

POST https://api.postpost.dev/api/v1/create-post
Content-Type: application/json
x-api-key: sk_your_api_key

{
  "content": "Your post content",
  "platforms": ["twitter-123456", "linkedin-ABC123"],
  "scheduledTime": "2026-03-15T14:00:00.000Z"
}

Response:

{
  "success": true,
  "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1"
}

Get Post Status

GET https://api.postpost.dev/api/v1/get-post/{postGroupId}
x-api-key: sk_your_api_key

Response:

{
  "postGroupId": "67a1b2c3d4e5f6a7b8c9d0e1",
  "status": "published",
  "content": "Your post content",
  "posts": [
    {
      "postId": "p_1",
      "platform": "twitter",
      "platformId": "123456",
      "status": "published",
      "publishedUrl": "https://twitter.com/user/status/123456"
    },
    {
      "postId": "p_2",
      "platform": "linkedin",
      "platformId": "ABC123",
      "status": "published",
      "publishedUrl": "https://linkedin.com/posts/..."
    }
  ]
}

Note: The get-post endpoint returns the raw platformId (e.g., "123456"), not the composite format (e.g., "twitter-123456"). The composite platform-platformId format is only returned by the list-posts and platform-connections endpoints.

Get Platform Connections

GET https://api.postpost.dev/api/v1/platform-connections
x-api-key: sk_your_api_key

Response:

{
  "success": true,
  "connections": [
    { "platformId": "twitter-123456789", "username": "myaccount", "displayName": "My Account", "tokenStatus": "unknown" },
    { "platformId": "linkedin-ABC123", "username": "My Company", "displayName": "My Company", "tokenStatus": "valid" },
    { "platformId": "threads-789012", "username": "mythreads", "displayName": "My Threads", "tokenStatus": "valid" }
  ]
}

Use the platformId field from connections as the platform identifier when creating posts.

Examples

JavaScript - PostPost API Client

class PostPostClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://api.postpost.dev/api/v1';
  }

  async request(endpoint, options = {}) {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'x-api-key': this.apiKey,
        'Content-Type': 'application/json',
        ...options.headers
      }
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error || `HTTP ${response.status}`);
    }

    return response.json();
  }

  async createPost(content, platforms, scheduledTime) {
    return this.request('/create-post', {
      method: 'POST',
      body: JSON.stringify({ content, platforms, scheduledTime })
    });
  }

  async listPosts(params = {}) {
    const query = new URLSearchParams(params).toString();
    return this.request(`/list-posts?${query}`);
  }
}

// Usage
const client = new PostPostClient('YOUR_API_KEY');

for (let i = 0; i < 100; i++) {
  await client.createPost(
    `Post ${i + 1}`,
    ['twitter-123'],
    new Date(Date.now() + i * 3600000).toISOString()
  );
}

Python - PostPost API Client with Retry

import requests
import time
from typing import Dict, Any, Optional

class PostPostClient:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = 'https://api.postpost.dev/api/v1'

    def _request(
        self,
        method: str,
        endpoint: str,
        data: Optional[Dict] = None,
        params: Optional[Dict] = None,
        max_retries: int = 3
    ) -> Dict[str, Any]:
        """Make a request with retry on failure."""

        headers = {
            'x-api-key': self.api_key,
            'Content-Type': 'application/json'
        }

        for attempt in range(max_retries):
            response = requests.request(
                method,
                f'{self.base_url}{endpoint}',
                headers=headers,
                json=data,
                params=params
            )

            if response.status_code >= 500:
                backoff = 2 ** attempt
                print(f"Server error. Retrying in {backoff}s (attempt {attempt + 1})")
                time.sleep(backoff)
                continue

            response.raise_for_status()
            return response.json()

        raise Exception("Max retries exceeded")

    def create_post(
        self,
        content: str,
        platforms: list,
        scheduled_time: Optional[str] = None
    ) -> Dict[str, Any]:
        data = {'content': content, 'platforms': platforms}
        if scheduled_time:
            data['scheduledTime'] = scheduled_time
        return self._request('POST', '/create-post', data=data)

    def list_posts(self, **params) -> Dict[str, Any]:
        return self._request('GET', '/list-posts', params=params)


# Usage
client = PostPostClient('YOUR_API_KEY')

# Bulk create
from datetime import datetime, timedelta, timezone

base_time = datetime.now(timezone.utc) + timedelta(hours=1)

for i in range(100):
    scheduled = (base_time + timedelta(hours=i)).isoformat()
    result = client.create_post(
        f"Scheduled post {i + 1}",
        ['twitter-123', 'linkedin-ABC'],
        scheduled
    )
    print(f"Created post {i + 1}: {result['postGroupId']}")

Peak Engagement Times

Optimal posting times vary by platform and audience. Use these guidelines as a starting point, then analyze your own analytics.

Platform-Specific Best Times (UTC)

PlatformBest DaysBest Hours (UTC)Peak Engagement
X/TwitterTue-Thu13:00-16:00Wednesday 12:00
LinkedInTue-Thu10:00-12:00Tuesday 10:00
InstagramMon, Wed11:00-14:00, 19:00-21:00Wednesday 11:00
ThreadsTue-Thu12:00-15:00Wednesday 13:00
TikTokTue-Thu15:00-21:00Thursday 19:00
YouTubeThu-Sat14:00-18:00Friday 15:00
FacebookWed-Fri13:00-16:00Wednesday 13:00
BlueskyMon-Fri14:00-17:00Tuesday 15:00

JavaScript - Optimal Time Scheduler

const OPTIMAL_TIMES = {
  twitter: {
    bestDays: [2, 3, 4], // Tue, Wed, Thu (0 = Sunday)
    bestHours: [13, 14, 15, 16],
    peakDay: 3,
    peakHour: 12
  },
  linkedin: {
    bestDays: [2, 3, 4],
    bestHours: [10, 11, 12],
    peakDay: 2,
    peakHour: 10
  },
  instagram: {
    bestDays: [1, 3], // Mon, Wed
    bestHours: [11, 12, 13, 14, 19, 20, 21],
    peakDay: 3,
    peakHour: 11
  },
  threads: {
    bestDays: [2, 3, 4],
    bestHours: [12, 13, 14, 15],
    peakDay: 3,
    peakHour: 13
  },
  tiktok: {
    bestDays: [2, 3, 4],
    bestHours: [15, 16, 17, 18, 19, 20, 21],
    peakDay: 4,
    peakHour: 19
  },
  facebook: {
    bestDays: [3, 4, 5], // Wed, Thu, Fri
    bestHours: [13, 14, 15, 16],
    peakDay: 3,
    peakHour: 13
  }
};

function getNextOptimalTime(platform, fromDate = new Date()) {
  const config = OPTIMAL_TIMES[platform];
  if (!config) throw new Error(`Unknown platform: ${platform}`);

  const result = new Date(fromDate);
  result.setUTCMinutes(0, 0, 0);

  // Find next best day
  let daysToAdd = 0;
  while (!config.bestDays.includes(result.getUTCDay()) || daysToAdd === 0) {
    result.setUTCDate(result.getUTCDate() + 1);
    daysToAdd++;
    if (daysToAdd > 7) break;
  }

  // Set to best hour
  result.setUTCHours(config.bestHours[0]);

  return result;
}

function getPeakTime(platform, weekStart = new Date()) {
  const config = OPTIMAL_TIMES[platform];
  if (!config) throw new Error(`Unknown platform: ${platform}`);

  const result = new Date(weekStart);
  result.setUTCHours(0, 0, 0, 0);

  // Move to peak day
  const currentDay = result.getUTCDay();
  const daysUntilPeak = (config.peakDay - currentDay + 7) % 7;
  result.setUTCDate(result.getUTCDate() + daysUntilPeak);

  // Set peak hour
  result.setUTCHours(config.peakHour);

  return result;
}

function distributePostsOptimally(platforms, count, startDate = new Date()) {
  const schedule = [];
  const date = new Date(startDate);

  for (let i = 0; i < count; i++) {
    const platform = platforms[i % platforms.length];
    const optimalTime = getNextOptimalTime(platform, date);

    schedule.push({
      index: i,
      platform,
      scheduledTime: optimalTime.toISOString()
    });

    // Move forward for next post
    date.setTime(optimalTime.getTime() + 3600000); // +1 hour minimum gap
  }

  return schedule;
}

// Usage
const schedule = distributePostsOptimally(
  ['twitter', 'linkedin', 'instagram'],
  9, // 3 posts per platform
  new Date()
);

console.log('Optimal posting schedule:');
schedule.forEach(item => {
  console.log(`${item.platform}: ${item.scheduledTime}`);
});

Python - Queue-Based Scheduler with Rate Limits

from datetime import datetime, timedelta, timezone
from typing import List, Dict, Optional
from dataclasses import dataclass
from collections import defaultdict
import heapq

@dataclass
class PlatformConfig:
    posts_per_hour: int
    posts_per_day: int
    best_hours: List[int]  # UTC hours
    best_days: List[int]   # 0=Monday, 6=Sunday

PLATFORM_LIMITS = {
    'twitter': PlatformConfig(
        posts_per_hour=10,
        posts_per_day=50,
        best_hours=[13, 14, 15, 16],
        best_days=[1, 2, 3]  # Tue, Wed, Thu
    ),
    'linkedin': PlatformConfig(
        posts_per_hour=5,
        posts_per_day=20,
        best_hours=[10, 11, 12],
        best_days=[1, 2, 3]
    ),
    'instagram': PlatformConfig(
        posts_per_hour=3,
        posts_per_day=10,
        best_hours=[11, 12, 13, 14, 19, 20, 21],
        best_days=[0, 2]  # Mon, Wed
    ),
    'tiktok': PlatformConfig(
        posts_per_hour=2,
        posts_per_day=10,
        best_hours=[15, 16, 17, 18, 19, 20, 21],
        best_days=[1, 2, 3]
    ),
}


class OptimalScheduler:
    """
    Schedule posts optimally across platforms considering:
    - Platform-specific rate limits
    - Peak engagement times
    - Even distribution across time slots
    """

    def __init__(self):
        self.platform_queues: Dict[str, List] = defaultdict(list)
        self.posts_per_hour: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
        self.posts_per_day: Dict[str, Dict[str, int]] = defaultdict(lambda: defaultdict(int))

    def get_next_optimal_slot(
        self,
        platform: str,
        after: datetime
    ) -> datetime:
        """Find the next available optimal time slot for a platform."""
        config = PLATFORM_LIMITS.get(platform)
        if not config:
            # Unknown platform - just space by 1 hour
            return after + timedelta(hours=1)

        candidate = after.replace(minute=0, second=0, microsecond=0)

        for _ in range(168):  # Check up to 1 week ahead
            candidate += timedelta(hours=1)
            day_key = candidate.strftime('%Y-%m-%d')
            hour_key = candidate.strftime('%Y-%m-%d-%H')

            # Check rate limits
            if self.posts_per_day[platform][day_key] >= config.posts_per_day:
                # Skip to next day
                candidate = candidate.replace(hour=0) + timedelta(days=1)
                continue

            if self.posts_per_hour[platform][hour_key] >= config.posts_per_hour:
                continue

            # Check if this is an optimal time
            if (candidate.weekday() in config.best_days and
                candidate.hour in config.best_hours):
                return candidate

        # If no optimal slot found, return next available
        return after + timedelta(hours=1)

    def schedule_post(
        self,
        platform: str,
        after: datetime = None
    ) -> datetime:
        """Schedule a single post and return its time."""
        if after is None:
            after = datetime.now(timezone.utc)

        scheduled_time = self.get_next_optimal_slot(platform, after)

        # Update counters
        day_key = scheduled_time.strftime('%Y-%m-%d')
        hour_key = scheduled_time.strftime('%Y-%m-%d-%H')
        self.posts_per_day[platform][day_key] += 1
        self.posts_per_hour[platform][hour_key] += 1

        return scheduled_time

    def schedule_batch(
        self,
        posts: List[Dict],
        start_after: datetime = None
    ) -> List[Dict]:
        """
        Schedule multiple posts across platforms optimally.

        Args:
            posts: List of {'content': str, 'platforms': List[str]}
            start_after: Earliest scheduling time

        Returns:
            List of posts with scheduledTime added
        """
        if start_after is None:
            start_after = datetime.now(timezone.utc)

        scheduled_posts = []
        platform_last_time: Dict[str, datetime] = {}

        for post in posts:
            platforms = post['platforms']
            # Find the latest "next optimal time" across all platforms
            scheduled_time = start_after

            for platform in platforms:
                platform_name = platform.split('-')[0]
                after = platform_last_time.get(platform_name, start_after)
                optimal_time = self.schedule_post(platform_name, after)

                if optimal_time > scheduled_time:
                    scheduled_time = optimal_time

                platform_last_time[platform_name] = scheduled_time

            scheduled_posts.append({
                **post,
                'scheduledTime': scheduled_time.isoformat()
            })

        return scheduled_posts


# Usage example
scheduler = OptimalScheduler()

# Posts to schedule
posts = [
    {'content': 'Morning motivation!', 'platforms': ['twitter-123', 'linkedin-ABC']},
    {'content': 'Product update announcement', 'platforms': ['twitter-123', 'linkedin-ABC', 'instagram-456']},
    {'content': 'Behind the scenes', 'platforms': ['instagram-456', 'tiktok-789']},
    {'content': 'Weekly tips thread', 'platforms': ['twitter-123']},
    {'content': 'Customer success story', 'platforms': ['linkedin-ABC']},
]

scheduled = scheduler.schedule_batch(posts)

print("Optimally scheduled posts:")
for post in scheduled:
    print(f"  {post['scheduledTime']}: {post['content'][:30]}...")
    print(f"    Platforms: {post['platforms']}")

Node.js - Complete Rate-Limited Queue System

const axios = require('axios');

class PostPostQueueScheduler {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.api = axios.create({
      baseURL: 'https://api.postpost.dev/api/v1',
      headers: { 'x-api-key': apiKey }
    });

    this.platformLimits = {
      twitter: { perHour: 10, perDay: 50 },
      linkedin: { perHour: 5, perDay: 20 },
      instagram: { perHour: 3, perDay: 10 },
      tiktok: { perHour: 2, perDay: 10 },
      facebook: { perHour: 5, perDay: 25 },
    };

    this.peakHours = {
      twitter: [13, 14, 15, 16],
      linkedin: [10, 11, 12],
      instagram: [11, 12, 13, 14, 19, 20, 21],
      tiktok: [15, 16, 17, 18, 19, 20, 21],
      facebook: [13, 14, 15, 16],
    };

    this.postCounts = { hourly: {}, daily: {} };
  }

  getPlatformName(platformId) {
    return platformId.split('-')[0];
  }

  getHourKey(date) {
    return date.toISOString().slice(0, 13);
  }

  getDayKey(date) {
    return date.toISOString().slice(0, 10);
  }

  canPostAt(platform, date) {
    const limits = this.platformLimits[platform];
    if (!limits) return true;

    const hourKey = `${platform}-${this.getHourKey(date)}`;
    const dayKey = `${platform}-${this.getDayKey(date)}`;

    const hourlyCount = this.postCounts.hourly[hourKey] || 0;
    const dailyCount = this.postCounts.daily[dayKey] || 0;

    return hourlyCount < limits.perHour && dailyCount < limits.perDay;
  }

  recordPost(platform, date) {
    const hourKey = `${platform}-${this.getHourKey(date)}`;
    const dayKey = `${platform}-${this.getDayKey(date)}`;

    this.postCounts.hourly[hourKey] = (this.postCounts.hourly[hourKey] || 0) + 1;
    this.postCounts.daily[dayKey] = (this.postCounts.daily[dayKey] || 0) + 1;
  }

  isPeakTime(platform, date) {
    const hours = this.peakHours[platform];
    return hours ? hours.includes(date.getUTCHours()) : true;
  }

  findNextOptimalSlot(platforms, after) {
    let candidate = new Date(after);
    candidate.setUTCMinutes(0, 0, 0);

    for (let i = 0; i < 168; i++) { // Check up to 1 week
      candidate = new Date(candidate.getTime() + 3600000); // +1 hour

      const allCanPost = platforms.every(p =>
        this.canPostAt(this.getPlatformName(p), candidate)
      );

      if (!allCanPost) continue;

      const anyPeak = platforms.some(p =>
        this.isPeakTime(this.getPlatformName(p), candidate)
      );

      if (anyPeak) {
        return candidate;
      }
    }

    // Fallback: return next hour
    return new Date(after.getTime() + 3600000);
  }

  async scheduleOptimally(posts) {
    const results = [];
    let lastTime = new Date();

    for (const post of posts) {
      const scheduledTime = this.findNextOptimalSlot(post.platforms, lastTime);

      // Record for rate limiting
      post.platforms.forEach(p => {
        this.recordPost(this.getPlatformName(p), scheduledTime);
      });

      try {
        const { data } = await this.api.post('/create-post', {
          content: post.content,
          platforms: post.platforms,
          scheduledTime: scheduledTime.toISOString()
        });

        results.push({
          success: true,
          postGroupId: data.postGroupId,
          scheduledTime: scheduledTime.toISOString(),
          content: post.content.slice(0, 50)
        });

        console.log(`Scheduled: ${post.content.slice(0, 30)}... at ${scheduledTime.toISOString()}`);
      } catch (error) {
        results.push({
          success: false,
          error: error.response?.data?.message || error.message,
          content: post.content.slice(0, 50)
        });
      }

      lastTime = scheduledTime;

      // Small delay between API calls
      await new Promise(r => setTimeout(r, 100));
    }

    return results;
  }
}

// Usage
const scheduler = new PostPostQueueScheduler('YOUR_API_KEY');

const posts = [
  { content: 'Morning motivation!', platforms: ['twitter-123', 'linkedin-ABC'] },
  { content: 'Product update', platforms: ['twitter-123', 'linkedin-ABC', 'instagram-456'] },
  { content: 'Behind the scenes', platforms: ['instagram-456', 'tiktok-789'] },
];

const results = await scheduler.scheduleOptimally(posts);
console.log(`Scheduled ${results.filter(r => r.success).length} of ${results.length} posts`);

Complete Queue-Based Scheduling System

This section provides a production-ready implementation that combines all the concepts: rate limiting, optimal scheduling, error handling, retry logic, and failed post recovery.

Architecture Overview

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  Content Queue  │───►│  Rate Limiter    │───►│  PostPost API    │
│  (your posts)   │    │  + Scheduler     │    │                 │
└─────────────────┘    └──────────────────┘    └─────────────────┘
                              │                        │
                              ▼                        ▼
                       ┌──────────────────┐    ┌─────────────────┐
                       │  Retry Queue     │    │  Status Checker │
                       │  (failed posts)  │    │  (polling)      │
                       └──────────────────┘    └─────────────────┘

JavaScript - Production Queue Scheduler

const axios = require('axios');

/**
 * Production-ready queue-based scheduler for PostPost API
 * Features:
 * - Platform-specific rate limiting
 * - Peak engagement time optimization
 * - Exponential backoff retry logic
 * - Failed post recovery
 * - Comprehensive error handling
 */
class PostPostQueueScheduler {
  constructor(apiKey, options = {}) {
    this.apiKey = apiKey;
    this.baseUrl = options.baseUrl || 'https://api.postpost.dev/api/v1';
    this.maxRetries = options.maxRetries || 3;
    this.retryDelay = options.retryDelay || 1000;

    // API rate limit tracking
    this.rateLimitRemaining = Infinity;
    this.rateLimitReset = 0;

    // Platform-specific limits (posts per hour / per day) - all 10 platforms
    this.platformLimits = {
      twitter:   { perHour: 10, perDay: 50 },
      linkedin:  { perHour: 5,  perDay: 20 },
      instagram: { perHour: 3,  perDay: 10 },
      threads:   { perHour: 10, perDay: 50 },
      tiktok:    { perHour: 2,  perDay: 10 },
      facebook:  { perHour: 5,  perDay: 25 },
      youtube:   { perHour: 2,  perDay: 10 },
      bluesky:   { perHour: 10, perDay: 100 },
      mastodon:  { perHour: 5,  perDay: 50 },
      telegram:  { perHour: 20, perDay: 300 },
    };

    // Peak engagement hours (UTC) - all 10 platforms
    this.peakHours = {
      twitter:   [13, 14, 15, 16],
      linkedin:  [10, 11, 12],
      instagram: [11, 12, 13, 14, 19, 20, 21],
      threads:   [12, 13, 14, 15],
      tiktok:    [15, 16, 17, 18, 19, 20, 21],
      facebook:  [13, 14, 15, 16],
      youtube:   [14, 15, 16, 17, 18],
      bluesky:   [14, 15, 16, 17],
      mastodon:  [14, 15, 16, 17],
      telegram:  [9, 10, 11, 12, 18, 19, 20],
    };

    // Track scheduled posts per platform
    this.postCounts = { hourly: {}, daily: {} };

    // Failed posts queue for retry
    this.failedQueue = [];

    // Results tracking
    this.results = [];
  }

  // ... (rest of the implementation)
}

On this page