Bonaventure OgetoBy Bonaventure Ogeto|

Payout Scheduling and Batching Strategies

Schedule payouts after your settlement cycle completes so funds are available. Batch transfers in groups of up to 100 for the bulk endpoint. Add 2-3 second delays between batches to respect rate limits. Run payouts during business hours for fastest settlement. For large payout runs, split across multiple execution windows to manage risk and rate limits.

Payout Scheduling Fundamentals

Paystack does not have a built-in scheduler. All transfers execute immediately when you call the API. Scheduling is your application's responsibility. You decide when to call the API, and you build the logic to determine which payouts are due at each execution time.

The scheduling decision depends on three factors: when your funds are available (settlement cycle), when your recipients expect to be paid (vendor agreements, payroll dates), and when the banking system processes transfers most efficiently (business hours, avoiding maintenance windows).

A simple scheduler uses a cron job that runs at fixed intervals and processes approved payouts. A sophisticated scheduler uses a job queue that handles retries, tracks execution state, and adapts to balance availability.

// Simple cron-based scheduler
// Run every day at 10:00 AM EAT
// crontab: 0 10 * * * node /app/scripts/run-payouts.js

async function scheduledPayoutRun() {
  var today = new Date();
  var dayOfWeek = today.getDay(); // 0=Sunday, 1=Monday, ...

  // Only run on weekdays
  if (dayOfWeek === 0 || dayOfWeek === 6) {
    console.log('Skipping payout run on weekend');
    return;
  }

  console.log('Starting scheduled payout run: ' + today.toISOString());

  var currencies = ['NGN', 'KES', 'GHS'];
  for (var i = 0; i < currencies.length; i++) {
    await executePayoutsForCurrency(currencies[i]);
  }

  console.log('Payout run complete');
}

Aligning Payouts with Settlement Cycles

Your settlement cycle determines when customer payment funds become available for transfers. If you try to run payouts before settlement completes, your available balance may be lower than expected.

T+1 settlement (next business day): Monday's payments are available Tuesday. Schedule vendor payouts for Tuesday's payments on Wednesday or later.

Weekly settlement: Payments from the previous week settle on a specific day. Schedule payouts for the day after settlement.

Build a buffer. Even with T+1, some payments may take slightly longer to settle. Running payouts on Wednesday for Monday's revenue gives you an extra day of buffer and catches any late settlements.

// Determine the safe payout date for a given revenue period
function getSafePayoutDate(revenueEndDate, settlementDays) {
  var payoutDate = new Date(revenueEndDate);

  // Add settlement days
  var daysAdded = 0;
  while (daysAdded < settlementDays + 1) { // +1 for buffer
    payoutDate.setDate(payoutDate.getDate() + 1);
    var day = payoutDate.getDay();
    if (day !== 0 && day !== 6) { // Skip weekends
      daysAdded++;
    }
  }

  return payoutDate;
}

// T+1 settlement, revenue period ends Sunday July 20
var payoutDate = getSafePayoutDate(new Date('2026-07-20'), 1);
// Returns Wednesday July 23 (Mon=T+1, Tue=buffer, Wed=payout)

Batching Strategies for Different Models

Marketplace vendor payouts (weekly)

Calculate each vendor's earnings for the week, create payout records, batch into groups of 100, and execute on the scheduled day. This is the most common pattern.

Driver/rider settlements (daily)

At the end of each day, calculate each driver's completed trips, deduct the platform commission, and batch the net amounts for transfer. Daily payouts require reliable automation because manual processing every day is not sustainable.

Payroll (monthly)

Process all employee salaries in a single batch run on the designated payroll date. Pre-fund the balance well before the payroll date to avoid surprises. See payroll disbursement with Paystack bulk transfers.

On-demand / instant payouts

No batching. When a vendor requests an instant payout, execute it immediately as a single transfer. This requires OTP to be disabled and a pre-funded balance. Set daily limits per vendor to prevent balance drain.

// Batch assembly for weekly vendor payouts
async function assembleWeeklyBatch(weekEndDate) {
  var vendors = await db.vendors.findActive();
  var payouts = [];

  for (var i = 0; i < vendors.length; i++) {
    var vendor = vendors[i];
    var earnings = await calculateVendorEarnings(vendor.id, weekEndDate);

    if (earnings.netAmount <= 0) continue; // No payout needed

    payouts.push({
      vendorId: vendor.id,
      recipientCode: vendor.paystackRecipientCode,
      amountMinorUnit: earnings.netAmount,
      currency: earnings.currency,
      reason: 'Weekly payout ending ' + weekEndDate,
      reference: 'weekly_' + vendor.id + '_' + weekEndDate,
    });
  }

  return payouts;
}

Rate Limit Management

Paystack enforces rate limits on API calls. If you send too many requests too quickly, you get throttled. Throttled requests return error responses, and your payout run stalls.

For bulk transfers, the main concern is the delay between consecutive batch submissions. Each batch of 100 is a single API call, but if you submit 10 batches back-to-back, that is 10 calls in quick succession.

async function executeWithRateLimiting(batches, currency) {
  var delayBetweenBatches = 3000; // 3 seconds

  for (var i = 0; i < batches.length; i++) {
    console.log(
      'Executing batch ' + (i + 1) + ' of ' + batches.length
      + ' (' + batches[i].length + ' transfers)'
    );

    try {
      await initiateBulkTransfer(currency, batches[i]);
    } catch (err) {
      if (err.message.indexOf('rate') !== -1 || err.message.indexOf('429') !== -1) {
        // Rate limited: wait longer and retry
        console.log('Rate limited. Waiting 30 seconds...');
        await new Promise(function(r) { setTimeout(r, 30000); });

        // Retry this batch
        try {
          await initiateBulkTransfer(currency, batches[i]);
        } catch (retryErr) {
          console.error('Batch ' + (i + 1) + ' failed on retry: ' + retryErr.message);
        }
      } else {
        console.error('Batch ' + (i + 1) + ' failed: ' + err.message);
      }
    }

    // Standard delay between batches
    if (i < batches.length - 1) {
      await new Promise(function(r) { setTimeout(r, delayBetweenBatches); });
    }
  }
}

Three seconds between batches is a safe starting point. If you still hit rate limits, increase to 5 seconds. If you never hit limits, you can try reducing to 2 seconds. Monitor your error rates and adjust.

Choosing Optimal Execution Windows

When you execute payouts affects settlement speed and success rate.

Best: 9 AM - 3 PM local time, weekdays. Banks are fully operational. Interbank clearing runs frequently. Most transfers settle within minutes to a few hours.

Acceptable: 3 PM - 9 PM local time, weekdays. Some banks start batch processing for end of day. Transfers may take longer but generally succeed.

Risky: 9 PM - 6 AM and weekends. Banks run maintenance, batch processing, and reconciliation. Higher chance of transient failures. Transfers may queue until the next business day.

Avoid: Month-end (25th-30th). Banking systems are under heavy load from payroll processing across the entire economy. Higher failure rates and longer settlement times.

M-Pesa and mobile money are exceptions. They operate 24/7 with consistent performance. If your payouts are primarily to M-Pesa wallets, execution timing is less critical.

Building a Production Scheduler

// Job-based scheduler with state tracking
async function createPayoutJob(jobType, currency, executionDate) {
  return await db.payoutJobs.create({
    jobType: jobType, // 'weekly_vendor', 'daily_driver', 'monthly_payroll'
    currency: currency,
    executionDate: executionDate,
    status: 'pending', // pending, running, completed, failed, partial
    totalPayouts: 0,
    completedPayouts: 0,
    failedPayouts: 0,
    createdAt: new Date(),
  });
}

async function runPayoutJob(jobId) {
  var job = await db.payoutJobs.findById(jobId);
  if (job.status !== 'pending') {
    console.log('Job already ' + job.status + '. Skipping.');
    return;
  }

  await db.payoutJobs.update(jobId, { status: 'running', startedAt: new Date() });

  try {
    // Assemble payouts for this job
    var payouts = await assemblePayoutsForJob(job);
    await db.payoutJobs.update(jobId, { totalPayouts: payouts.length });

    if (payouts.length === 0) {
      await db.payoutJobs.update(jobId, {
        status: 'completed',
        completedAt: new Date(),
        note: 'No payouts to process',
      });
      return;
    }

    // Execute with balance check and batching
    await executePayoutBatch(payouts, job.currency, jobId);

    // Final status will be set by the reconciliation check
    await db.payoutJobs.update(jobId, {
      status: 'completed',
      completedAt: new Date(),
    });
  } catch (err) {
    await db.payoutJobs.update(jobId, {
      status: 'failed',
      note: err.message,
    });
    await alertOps('Payout job ' + jobId + ' failed: ' + err.message);
  }
}

Track each payout run as a "job" with its own status. This gives your operations team visibility into what ran, when, and what the outcome was. Build a simple admin page that shows recent payout jobs with their status, payout count, success rate, and any errors.

For the full automated payout system architecture, see building an automated payout system on Paystack.

Build Reliable Payout Operations

Payout scheduling is where engineering meets operations. Getting the timing, batching, and monitoring right means your vendors get paid on time, every time.

The McTaba bootcamp covers the operational side of payment systems alongside the code. You learn to build systems that work in production, not just in test mode.

Create a free McTaba account

Key Takeaways

  • Schedule payouts at least one full settlement cycle after the revenue period ends. If settlement is T+1, run weekly payouts on Wednesday for the previous Monday-Sunday period.
  • Batch transfers in groups of 100 using the bulk transfer endpoint. Add 2-3 second delays between batches to stay within rate limits.
  • Run payouts during business hours (9 AM - 3 PM local time) for the fastest bank settlement. Avoid late night and early morning when banks run maintenance.
  • For large payout runs (500+ transfers), split across multiple execution windows to reduce risk and allow partial completion tracking.
  • Build a scheduler with cron or a job queue. Track each execution run with a batch ID so you can monitor progress and handle failures per batch.
  • Separate scheduling (when) from execution (how). Let the scheduler determine timing, and let the execution engine handle the Paystack API calls.

Frequently Asked Questions

Can Paystack schedule transfers for a future date?
No. Paystack processes transfers immediately when you call the API. All scheduling logic must be built in your application using cron jobs, job queues, or task schedulers. This gives you more control over timing, approval, and error handling than a built-in scheduler would.
How should I handle a payout job that fails mid-execution?
The payouts that already went to Paystack continue processing. Payouts not yet sent remain in their current state. Your job should track which payouts were submitted and which were not. On the next run, only process the remaining ones. Deterministic references prevent duplicates if some payouts overlap between runs.
What is the best payout frequency for a marketplace?
Weekly payouts are the most common starting point for marketplaces. They balance vendor expectations (regular income) with operational overhead (one payout run per week). As you grow, consider offering faster payout options (daily or instant) as a premium feature for top vendors.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon