Paystack API timeouts happen on the client side, not on Paystack. Your HTTP client gave up waiting before the server responded. Set your timeout to 15-30 seconds (not the default 5 seconds many libraries use). Use AbortController in Node.js to enforce the timeout. For retries, only retry idempotent endpoints like /transaction/verify and /transfer/verify. Never retry /transaction/initialize without checking whether the first request actually went through, because Paystack may have created the transaction even though your client timed out. Use exponential backoff with jitter: wait 1 second, then 2 seconds, then 4 seconds, plus a random 0-500ms.
When you get a timeout error calling Paystack, it does not mean Paystack is down. It means your HTTP client waited for a response, hit its time limit, and gave up. The important thing: Paystack may have received and processed your request. You just never got the response.
This is what a timeout looks like in Node.js:
// With fetch + AbortController
AbortError: The operation was aborted
// With axios
Error: timeout of 5000ms exceeded
// With node-fetch
FetchError: network timeout at: https://api.paystack.co/transaction/initialize
The danger is acting on the timeout as if the request failed. If you called /transaction/initialize and timed out, the transaction may exist on Paystack. If you retry, you create a second transaction. If you called /transaction/charge_authorization and timed out, the charge may have gone through. Retrying charges the customer twice.
Timeouts are not the same as errors. An error (4xx, 5xx) means Paystack received your request and rejected it. A timeout means you have no idea what happened on the server side.
There are four common causes of Paystack API timeouts.
1. Your client timeout is too short. Many HTTP libraries default to 5 seconds. Paystack API calls that involve bank communication (initialize, charge, verify during processing) can take 10-20 seconds. A 5-second timeout will cut the connection before the bank responds.
2. Your server is behind a proxy with its own timeout. Nginx defaults to 60 seconds, but some configurations set it lower. AWS API Gateway defaults to 29 seconds. If your proxy times out before your Node.js client does, you get a 504 Gateway Timeout from the proxy, not from Paystack.
3. DNS resolution is slow. If your server cannot resolve api.paystack.co quickly (common on some VPS providers or Docker containers without proper DNS config), the DNS lookup eats into your timeout budget. A 5-second timeout with a 3-second DNS lookup leaves only 2 seconds for the actual API call.
4. Paystack is experiencing high load. During peak periods (month-end salary payments, Black Friday sales), Paystack response times increase. A timeout value that works at 2am may not work at noon on payday. Check status.paystack.com if timeouts suddenly spike.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Use AbortController to add timeout behavior to Node.js fetch calls. The native fetch API does not have a timeout option, so you create an abort signal that fires after your chosen duration.
// lib/paystack-client.js
function createPaystackRequest(endpoint, options) {
var timeout = options.timeout || 30000; // 30 seconds default
var controller = new AbortController();
var timeoutId = setTimeout(function() {
controller.abort();
}, timeout);
var url = 'https://api.paystack.co' + endpoint;
var fetchOptions = {
method: options.method || 'GET',
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
signal: controller.signal,
};
if (options.body) {
fetchOptions.body = JSON.stringify(options.body);
}
return fetch(url, fetchOptions)
.then(function(res) {
clearTimeout(timeoutId);
return res.json();
})
.catch(function(err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
var error = new Error(
'Paystack API timeout after ' + timeout + 'ms on ' + endpoint
);
error.code = 'PAYSTACK_TIMEOUT';
error.endpoint = endpoint;
error.timeout = timeout;
throw error;
}
throw err;
});
}
// Usage
createPaystackRequest('/transaction/verify/' + reference, {
method: 'GET',
timeout: 15000, // 15 seconds for verify
});
createPaystackRequest('/transaction/initialize', {
method: 'POST',
body: { email: email, amount: amount },
timeout: 30000, // 30 seconds for initialize
});
Recommended timeout values by endpoint type:
The word "idempotent" means calling the endpoint twice produces the same result as calling it once. For Paystack:
Safe to retry (idempotent):
GET /transaction/verify/:reference - Reading data, no side effectsGET /transaction/:id - Reading dataGET /transfer/verify/:reference - Reading dataGET /customer/:email_or_code - Reading dataNOT safe to retry blindly:
POST /transaction/initialize - Creates a new transaction each time. Retrying creates duplicates.POST /transaction/charge_authorization - Charges the customer. Retrying double-charges.POST /transfer - Sends money. Retrying sends money twice.POST /refund - Refunds money. Retrying may double-refund.Safe to retry WITH your own idempotency key:
POST /transaction/initialize - Safe if you use the same reference. Paystack will reject the second call with "Duplicate Transaction Reference" instead of creating a new transaction.POST /transfer - Safe if you use the same reference. Paystack rejects duplicates.The pattern: always generate your reference before calling the API, not after. That way, if you need to retry, you send the same reference and Paystack's duplicate detection protects you.
When you retry, do not fire the second request immediately. Use exponential backoff: wait longer between each attempt. Add jitter (randomness) so that if 50 customers all time out at the same moment, they do not all retry at the same moment and overload Paystack again.
// lib/retry.js
function wait(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
}
function calculateBackoff(attempt, baseDelay, maxDelay) {
// Exponential: 1s, 2s, 4s, 8s...
var exponentialDelay = baseDelay * Math.pow(2, attempt);
// Cap at maxDelay
var capped = Math.min(exponentialDelay, maxDelay);
// Add jitter: random 0 to 500ms
var jitter = Math.floor(Math.random() * 500);
return capped + jitter;
}
function retryWithBackoff(fn, options) {
var maxRetries = options.maxRetries || 3;
var baseDelay = options.baseDelay || 1000;
var maxDelay = options.maxDelay || 10000;
var shouldRetry = options.shouldRetry || function() { return true; };
var onRetry = options.onRetry || function() {};
function attempt(retryCount) {
return fn().catch(function(err) {
if (retryCount >= maxRetries || !shouldRetry(err)) {
throw err;
}
var delay = calculateBackoff(retryCount, baseDelay, maxDelay);
onRetry(err, retryCount + 1, delay);
return wait(delay).then(function() {
return attempt(retryCount + 1);
});
});
}
return attempt(0);
}
module.exports = { retryWithBackoff, calculateBackoff };
The backoff schedule with a 1-second base delay:
Three retries with exponential backoff means a total wait of roughly 7 to 8 seconds. That is enough time for a temporary network issue to resolve without making the user wait an unreasonable amount of time.
Verification is the most common endpoint you need to retry. If your verify call times out, you must know whether the payment succeeded. Here is a complete pattern:
// lib/paystack-verify.js
var retryWithBackoff = require('./retry').retryWithBackoff;
function verifyTransaction(reference) {
return retryWithBackoff(
function() {
var controller = new AbortController();
var timeoutId = setTimeout(function() {
controller.abort();
}, 15000);
return fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
signal: controller.signal,
}
)
.then(function(res) {
clearTimeout(timeoutId);
if (!res.ok && res.status !== 400) {
if (res.status >= 500 || res.status === 429) {
var error = new Error('Paystack returned ' + res.status);
error.retryable = true;
throw error;
}
}
return res.json();
})
.catch(function(err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
var timeoutError = new Error('Verify timeout for ' + reference);
timeoutError.retryable = true;
throw timeoutError;
}
if (err.code === 'ECONNRESET' || err.code === 'ENOTFOUND') {
err.retryable = true;
}
throw err;
});
},
{
maxRetries: 3,
baseDelay: 1000,
shouldRetry: function(err) { return err.retryable === true; },
onRetry: function(err, attempt, delay) {
console.log(
'Retry verify ' + reference +
' attempt ' + attempt +
' after ' + delay + 'ms: ' + err.message
);
},
}
);
}
module.exports = { verifyTransaction };
This pattern retries on timeouts, 5xx server errors, and 429 rate limits. It does not retry on 4xx client errors (bad reference, invalid key) because those will fail every time.
You can retry /transaction/initialize safely, but only if you generate the reference before the first call. Paystack rejects duplicate references, so the second call will either succeed (if the first never reached Paystack) or return a duplicate reference error (if the first did reach Paystack and created the transaction).
// lib/paystack-initialize.js
var crypto = require('crypto');
function generateReference() {
return 'txn_' + Date.now() + '_' + crypto.randomBytes(6).toString('hex');
}
function initializeWithRetry(email, amount, metadata) {
// Generate reference BEFORE the first attempt
var reference = generateReference();
return retryWithBackoff(
function() {
var controller = new AbortController();
var timeoutId = setTimeout(function() {
controller.abort();
}, 30000);
return fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: amount,
reference: reference,
metadata: metadata,
}),
signal: controller.signal,
})
.then(function(res) {
clearTimeout(timeoutId);
return res.json();
})
.then(function(data) {
if (!data.status && data.message &&
data.message.toLowerCase().includes('duplicate')) {
console.log('Duplicate reference ' + reference + ', first request succeeded');
return verifyAndReturnUrl(reference);
}
return data;
})
.catch(function(err) {
clearTimeout(timeoutId);
if (err.name === 'AbortError') {
var timeoutError = new Error('Initialize timeout');
timeoutError.retryable = true;
throw timeoutError;
}
throw err;
});
},
{
maxRetries: 2,
baseDelay: 2000,
shouldRetry: function(err) { return err.retryable === true; },
onRetry: function(err, attempt) {
console.log('Retry initialize ' + reference + ' attempt ' + attempt);
},
}
);
}
function verifyAndReturnUrl(reference) {
return fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
)
.then(function(res) { return res.json(); })
.then(function(data) {
return {
status: true,
data: {
authorization_url: data.data.authorization_url,
reference: reference,
},
};
});
}
The key insight: by generating the reference first and sending the same reference on retry, you turn a non-idempotent endpoint into a safe one. If the first request succeeded, the duplicate detection catches it. If the first request never reached Paystack, the retry works normally.
The /transaction/charge_authorization endpoint charges a saved card. If this call times out, the charge may have gone through. Retrying blindly double-charges the customer.
The safe pattern:
// lib/safe-charge.js
function chargeWithSafeRetry(authorizationCode, email, amount) {
var reference = generateReference();
return attemptCharge(authorizationCode, email, amount, reference)
.catch(function(err) {
if (err.code !== 'PAYSTACK_TIMEOUT') {
throw err;
}
console.log('Charge timed out for ' + reference + ', verifying...');
return fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
)
.then(function(res) { return res.json(); })
.then(function(verifyResult) {
if (verifyResult.data && verifyResult.data.status === 'success') {
console.log('Charge ' + reference + ' succeeded (post-timeout verify)');
return verifyResult;
}
if (verifyResult.data && verifyResult.data.status === 'failed') {
console.log('Charge ' + reference + ' failed, retrying...');
var newReference = generateReference();
return attemptCharge(authorizationCode, email, amount, newReference);
}
console.log('Charge ' + reference + ' status unclear, waiting...');
return waitAndVerify(reference, 3);
});
});
}
function waitAndVerify(reference, attemptsLeft) {
if (attemptsLeft <= 0) {
var err = new Error(
'Could not determine charge status for ' + reference
);
err.reference = reference;
err.action = 'manual_check';
throw err;
}
return wait(5000).then(function() {
return fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
'Authorization': 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
)
.then(function(res) { return res.json(); })
.then(function(result) {
if (result.data && result.data.status === 'success') {
return result;
}
if (result.data && result.data.status === 'failed') {
return result;
}
return waitAndVerify(reference, attemptsLeft - 1);
});
});
}
This pattern is more complex than simple retry, but it prevents the worst outcome in payment processing: charging a customer twice. The extra complexity is worth it.
Log every timeout with enough context to diagnose patterns. Good timeout logs tell you which endpoint is slow, when it happens, and how many retries it took.
// lib/timeout-logger.js
function logTimeout(endpoint, reference, attempt, timeout, startTime) {
var elapsed = Date.now() - startTime;
var logEntry = {
event: 'paystack_timeout',
endpoint: endpoint,
reference: reference || null,
attempt: attempt,
timeout_ms: timeout,
elapsed_ms: elapsed,
timestamp: new Date().toISOString(),
};
console.error(JSON.stringify(logEntry));
}
// Alert if timeouts exceed a threshold
var timeoutCounts = {};
var ALERT_THRESHOLD = 5;
var WINDOW_MS = 60000;
function trackTimeoutRate(endpoint) {
var now = Date.now();
if (!timeoutCounts[endpoint]) {
timeoutCounts[endpoint] = [];
}
timeoutCounts[endpoint].push(now);
timeoutCounts[endpoint] = timeoutCounts[endpoint].filter(function(t) {
return t > now - WINDOW_MS;
});
if (timeoutCounts[endpoint].length >= ALERT_THRESHOLD) {
console.error(
'ALERT: ' + timeoutCounts[endpoint].length +
' Paystack timeouts on ' + endpoint +
' in the last ' + (WINDOW_MS / 1000) + ' seconds'
);
}
}
When you see a pattern of timeouts on a specific endpoint, check three things in order: your client timeout setting, status.paystack.com for known incidents, and your server's network connectivity (DNS, firewalls, proxy timeouts).
You cannot easily make Paystack time out on demand, but you can test your timeout and retry code by simulating slow responses.
Step 1: Test AbortController timeout.
Create a test server that delays responses, then point your Paystack client at it:
// test/slow-server.js
var http = require('http');
http.createServer(function(req, res) {
// Simulate a 20-second response delay
setTimeout(function() {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: true, data: {} }));
}, 20000);
}).listen(3999);
// Your 15-second timeout should trigger before the 20-second delay
Step 2: Verify retry count and backoff timing.
Add timing logs to your retry function and verify the delays match your backoff schedule. First retry after about 1 second, second after about 2 seconds, third after about 4 seconds.
Step 3: Test duplicate reference handling.
In Paystack test mode, call /transaction/initialize twice with the same reference. The second call should return a "Duplicate Transaction Reference" error. Verify your retry handler catches this and verifies instead of failing.
Step 4: Verify your logs.
After triggering a timeout in test mode, check that your structured log includes the endpoint, reference, attempt number, timeout value, and elapsed time. This data is what you need when debugging production timeout issues at 3am.
Step 5: Test the charge safety pattern.
Use Paystack test mode to verify the charge-then-verify flow. Initialize a transaction, complete it, then call your chargeWithSafeRetry function with the same authorization code. Simulate a timeout on the charge call and verify that your code checks verification before retrying.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course