Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/api/apiUtils/rateLimit/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const { rateLimitDefaultConfigCacheTTL, rateLimitDefaultBurstCapacity } = requir
*
* // Optional: Number of CloudServer nodes in distributed deployment
* // Default: 1
* // Used to calculate per-worker rate limit: limit / nodes / workers
* // Used to calculate per-node rate limit: limit / nodes
* // Workers on the same node share the per-node quota
* "nodes": 1,
*
* // Optional: Bucket-level rate limiting configuration
Expand Down
14 changes: 5 additions & 9 deletions lib/api/bucketPutRateLimit.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,15 @@ function validateRateLimitConfig(requestConfig, callback) {
.customizeDescription('RequestsPerSecond must be a positive integer'));
}

// Validate limit against node count AND worker count
// Each node enforces limit/nodes locally; workers on a node share that quota
const nodes = config.rateLimiting?.nodes || 1;
const workers = config.clusters || 1;
const minLimit = nodes * workers;

if (limit > 0 && limit < minLimit) {
if (limit > 0 && limit < nodes) {
return callback(errorInstances.InvalidArgument
.customizeDescription(
`RequestsPerSecond (${limit}) must be >= ` +
`(nodes x workers = ${nodes} x ${workers} = ${minLimit}) or 0 (unlimited). ` +
'Each worker enforces limit/nodes/workers locally. ' +
`With limit less than ${minLimit}, per-worker rate would be less than 1 req/s, ` +
'effectively blocking traffic.'
`RequestsPerSecond (${limit}) must be >= nodes (${nodes}) or 0 (unlimited). ` +
'Each node enforces limit/nodes locally. ' +
`With limit < ${nodes}, per-node rate would be < 1 req/s, effectively blocking traffic.`
Comment on lines +33 to +35

@anurag4DSB anurag4DSB Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are using only nodes, as we run 10 processes per node, does that mean we will have 10x the limit allowed on a node?
I don't get it, i thought we are using nodes * workers as we are dividing the node limit among workers on that node and thats the reason for the limit to be atleast higher than nodes* workers. Has something changed?

Example: lets say there are 6 nodes, and we have 10 workers per node, how will 1 req/s be divided among 10 workers

));
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenko/cloudserver",
"version": "9.2.42",
"version": "9.2.43",
"description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol",
"main": "index.js",
"engines": {
Expand Down
16 changes: 7 additions & 9 deletions tests/functional/aws-node-sdk/test/bucket/putBucketRateLimit.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,15 @@ skipIfRateLimitDisabled('Test put bucket rate limit', () => {
}
});

describe('validation against node and worker count', () => {
describe('validation against node count', () => {

const nodes = config.rateLimiting?.nodes || 1;
const workers = config.clusters || 1;
const minLimit = nodes * workers;
const skipIfSingleNode = nodes === 1 ? it.skip : it;
const minLimit = config.rateLimiting?.nodes || 1;
const skipIfSingleNode = minLimit === 1 ? it.skip : it;

// Test requires multiple nodes to pass
// With only 1 node and 1 worker the minLimit is 1.
// With only 1 node the minLimit is 1.
// This leaves no invalid values to test as 0 is also a valid setting (unlimited)
skipIfSingleNode('should reject limits less than (nodes x workers)', async () => {
skipIfSingleNode('should reject limits less than nodes', async () => {
let error;
try {
const invalidConfig = { RequestsPerSecond: minLimit - 1 };
Expand All @@ -136,7 +134,7 @@ skipIfRateLimitDisabled('Test put bucket rate limit', () => {
}
});

it('should accept limits equal to (nodes x workers)', async () => {
it('should accept limits equal to nodes', async () => {
try {
const validConfig = { RequestsPerSecond: minLimit };
await sendRateLimitRequest('PUT', '127.0.0.1:8000',
Expand All @@ -150,7 +148,7 @@ skipIfRateLimitDisabled('Test put bucket rate limit', () => {
}
});

it('should accept limits greater than (nodes x workers)', async () => {
it('should accept limits greater than nodes', async () => {
try {
const validConfig = { RequestsPerSecond: minLimit + 1000 };
await sendRateLimitRequest('PUT', '127.0.0.1:8000',
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/api/bucketPutRateLimit.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ describe('bucketPutRateLimit API', () => {
});
});

it('should return InvalidArgument error if RequestsPerSecond is less than nodes', done => {
config.rateLimiting.nodes = 3;
bucketPut(regularAuthInfo, bucketPutReq, log, err => {
assert.ifError(err);
const putRequest = getRateLimitPutRequest(bucketName, { RequestsPerSecond: 2 });
bucketPutRateLimit(rateLimitServiceAuthInfo, putRequest, log, err => {
assert.strictEqual(err.is.InvalidArgument, true);
assert.match(err.description, /must be >= nodes \(3\)/);
done();
});
});
});

it('should return InvalidArgument error if request body is invalid JSON', done => {
bucketPut(regularAuthInfo, bucketPutReq, log, err => {
assert.ifError(err);
Expand Down Expand Up @@ -208,6 +221,23 @@ describe('bucketPutRateLimit API success cases', () => {
});
});

it('should accept RequestsPerSecond equal to nodes regardless of worker count', done => {
config.rateLimiting.nodes = 3;
sandbox.stub(config, 'clusters').value(10);
bucketPut(regularAuthInfo, bucketPutReq, log, err => {
assert.ifError(err);
const putRequest = getRateLimitPutRequest(bucketName, { RequestsPerSecond: 3 });
bucketPutRateLimit(rateLimitServiceAuthInfo, putRequest, log, err => {
assert.ifError(err);
metadata.getBucket(bucketName, log, (err, bucket) => {
assert.ifError(err);
assert.strictEqual(bucket.getRateLimitConfiguration().getData().RequestsPerSecond.Limit, 3);
done();
});
});
});
});

it('should allow zero as a valid RequestsPerSecond value', done => {
bucketPut(regularAuthInfo, bucketPutReq, log, err => {
assert.ifError(err);
Expand Down
Loading