From 984e8aa15b8ecf8d5b337b7098c69f98f8f086a8 Mon Sep 17 00:00:00 2001 From: Sihyeon Jang Date: Tue, 18 Nov 2025 14:55:43 +0900 Subject: [PATCH 1/4] feat: add window_type for limit-count Signed-off-by: Sihyeon Jang --- apisix/plugins/limit-count/init.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/limit-count/init.lua b/apisix/plugins/limit-count/init.lua index 1f37965c4547..7241bd2eeb6e 100644 --- a/apisix/plugins/limit-count/init.lua +++ b/apisix/plugins/limit-count/init.lua @@ -72,6 +72,11 @@ local schema = { properties = { count = {type = "integer", exclusiveMinimum = 0}, time_window = {type = "integer", exclusiveMinimum = 0}, + window_type = { + type = "string", + enum = {"fixed", "sliding"}, + default = "fixed", + }, group = {type = "string"}, key = {type = "string", default = "remote_addr"}, key_type = {type = "string", @@ -137,6 +142,12 @@ function _M.check_schema(conf, schema_type) return false, err end + if (not conf.policy or conf.policy == "local") + and conf.window_type and conf.window_type ~= "fixed" + then + return false, "window_type \"sliding\" is only supported when policy is \"redis\" or \"redis-cluster\"" + end + if conf.group then -- means that call by some plugin not support if conf._vid then @@ -184,12 +195,12 @@ local function create_limit_obj(conf, plugin_name) if conf.policy == "redis" then return limit_redis_new("plugin-" .. plugin_name, - conf.count, conf.time_window, conf) + conf.count, conf.time_window, conf.window_type, conf) end if conf.policy == "redis-cluster" then return limit_redis_cluster_new("plugin-" .. plugin_name, conf.count, - conf.time_window, conf) + conf.time_window, conf.window_type, conf) end return nil From 9da2fcb2b05a27b9e1d22c10752e0842028729f8 Mon Sep 17 00:00:00 2001 From: Sihyeon Jang Date: Tue, 18 Nov 2025 14:56:27 +0900 Subject: [PATCH 2/4] feat: add limit-count sliding window for redis with tests Signed-off-by: Sihyeon Jang --- .../plugins/limit-count/limit-count-redis.lua | 77 ++- t/plugin/limit-count-redis-sliding.t | 447 ++++++++++++++++++ 2 files changed, 515 insertions(+), 9 deletions(-) create mode 100644 t/plugin/limit-count-redis-sliding.t diff --git a/apisix/plugins/limit-count/limit-count-redis.lua b/apisix/plugins/limit-count/limit-count-redis.lua index c40ed437f342..9d9358c8aa2a 100644 --- a/apisix/plugins/limit-count/limit-count-redis.lua +++ b/apisix/plugins/limit-count/limit-count-redis.lua @@ -21,7 +21,7 @@ local setmetatable = setmetatable local tostring = tostring -local _M = {version = 0.3} +local _M = {version = 0.4} local mt = { @@ -29,7 +29,7 @@ local mt = { } -local script = core.string.compress_script([=[ +local script_fixed = core.string.compress_script([=[ assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1") local ttl = redis.call('ttl', KEYS[1]) if ttl < 0 then @@ -40,12 +40,61 @@ local script = core.string.compress_script([=[ ]=]) -function _M.new(plugin_name, limit, window, conf) +local script_sliding = core.string.compress_script([=[ + assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1") + + local now = tonumber(ARGV[1]) + local window = tonumber(ARGV[2]) + local limit = tonumber(ARGV[3]) + local cost = tonumber(ARGV[4]) + + local window_start = now - window + + -- remove events outside of the window + redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start) + + local current = redis.call('ZCARD', KEYS[1]) + + if current + cost > limit then + local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') + local reset = 0 + if #earliest == 2 then + reset = earliest[2] + window - now + if reset < 0 then + reset = 0 + end + end + return {-1, reset} + end + + for i = 1, cost do + redis.call('ZADD', KEYS[1], now, now .. ':' .. i) + end + + redis.call('PEXPIRE', KEYS[1], window) + + local remaining = limit - (current + cost) + + local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') + local reset = 0 + if #earliest == 2 then + reset = earliest[2] + window - now + if reset < 0 then + reset = 0 + end + end + + return {remaining, reset} +]=]) + + +function _M.new(plugin_name, limit, window, window_type, conf) assert(limit > 0 and window > 0) local self = { limit = limit, window = window, + window_type = window_type or "fixed", conf = conf, plugin_name = plugin_name, } @@ -59,13 +108,22 @@ function _M.incoming(self, key, cost) return red, err, 0 end - local limit = self.limit - local window = self.window - local res key = self.plugin_name .. tostring(key) local ttl = 0 - res, err = red:eval(script, 1, key, limit, window, cost or 1) + local limit = self.limit + local c = cost or 1 + local res + + if self.window_type == "sliding" then + local now = ngx.now() * 1000 + local window = self.window * 1000 + + res, err = red:eval(script_sliding, 1, key, now, window, limit, c) + else + local window = self.window + res, err = red:eval(script_fixed, 1, key, limit, window, c) + end if err then return nil, err, ttl @@ -74,14 +132,15 @@ function _M.incoming(self, key, cost) local remaining = res[1] ttl = res[2] - local ok, err = red:set_keepalive(10000, 100) + local ok, err2 = red:set_keepalive(10000, 100) if not ok then - return nil, err, ttl + return nil, err2, ttl end if remaining < 0 then return nil, "rejected", ttl end + return 0, remaining, ttl end diff --git a/t/plugin/limit-count-redis-sliding.t b/t/plugin/limit-count-redis-sliding.t new file mode 100644 index 000000000000..2b14f0c668c1 --- /dev/null +++ b/t/plugin/limit-count-redis-sliding.t @@ -0,0 +1,447 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: redis policy with sliding window - basic N per window +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 2, + "window_type": "sliding", + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_timeout": 1000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + +=== TEST 2: redis policy with sliding window - enforce N per window +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 503] + + +=== TEST 3: redis policy with sliding window - remaining header on reject +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + + -- ensure previous windows are expired before starting this test + ngx.sleep(2.2) + + -- first request: allowed, remaining should be 1 + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, {res.status, res.headers["X-RateLimit-Remaining"]}) + end + + -- second request: allowed, remaining should be 0 + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, {res.status, res.headers["X-RateLimit-Remaining"]}) + end + + -- third request: rejected, remaining header should stay at 0 + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, {res.status, res.headers["X-RateLimit-Remaining"]}) + end + + ngx.say(json.encode(ress)) + } + } +--- response_body +[[200,"1"],[200,"0"],[503,"0"]] + + +=== TEST 4: redis policy with sliding window - allow after window passes +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local codes = {} + + -- ensure previous windows are expired before starting this test + ngx.sleep(2.2) + + -- consume full quota + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(codes, res.status) + end + + -- wait longer than the sliding window (2s) + ngx.sleep(2.2) + + -- should be allowed again after window has passed + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(codes, res.status) + end + + ngx.say(json.encode(codes)) + } + } +--- response_body +[200,200,200] + + + +=== TEST 5: setup route with fixed window for boundary burst comparison +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "uri": "/fixed", + "plugins": { + "limit-count": { + "count": 4, + "time_window": 4, + "window_type": "fixed", + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_timeout": 1000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 6: setup route with sliding window for boundary burst comparison +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/3', + ngx.HTTP_PUT, + [[{ + "uri": "/sliding", + "plugins": { + "limit-count": { + "count": 4, + "time_window": 4, + "window_type": "sliding", + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_timeout": 1000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 7: sliding window - cost parameter support +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/4', + ngx.HTTP_PUT, + [[{ + "uri": "/sliding-cost", + "plugins": { + "limit-count": { + "count": 10, + "time_window": 3, + "window_type": "sliding", + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis", + "redis_host": "127.0.0.1", + "redis_port": 6379, + "redis_timeout": 1000 + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 8: sliding window - verify X-RateLimit headers accuracy +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/sliding-cost" + local results = {} + + -- ensure previous windows are expired + ngx.sleep(3.5) + + -- Send requests and check headers + for i = 1, 5 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say("error: " .. err) + return + end + + local limit = res.headers["X-RateLimit-Limit"] + local remaining = res.headers["X-RateLimit-Remaining"] + local reset = res.headers["X-RateLimit-Reset"] + + table.insert(results, { + req = i, + status = res.status, + limit = limit, + remaining = remaining, + has_reset = reset ~= nil + }) + end + + for _, r in ipairs(results) do + ngx.say(string.format("req %d: status=%d, limit=%s, remaining=%s, has_reset=%s", + r.req, r.status, r.limit or "nil", r.remaining or "nil", tostring(r.has_reset))) + end + } + } +--- response_body_like +req 1: status=404, limit=10, remaining=9, has_reset=true +req 2: status=404, limit=10, remaining=8, has_reset=true +req 3: status=404, limit=10, remaining=7, has_reset=true +req 4: status=404, limit=10, remaining=6, has_reset=true +req 5: status=404, limit=10, remaining=5, has_reset=true + + + +=== TEST 9: verify local policy rejects sliding window +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/5', + ngx.HTTP_PUT, + [[{ + "uri": "/local-sliding", + "plugins": { + "limit-count": { + "count": 10, + "time_window": 60, + "window_type": "sliding", + "rejected_code": 503, + "key": "remote_addr", + "policy": "local" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + else + ngx.say("ERROR: should have been rejected") + end + } + } +--- error_code: 400 + + + +=== TEST 10: sliding window with redis-cluster policy +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + -- Note: This test requires redis-cluster to be available + -- It validates schema but may fail at runtime if cluster unavailable + local code, body = t('/apisix/admin/routes/6', + ngx.HTTP_PUT, + [[{ + "uri": "/cluster-sliding", + "plugins": { + "limit-count": { + "count": 10, + "time_window": 60, + "window_type": "sliding", + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis-cluster", + "redis_cluster_nodes": [ + "127.0.0.1:5000", + "127.0.0.1:5001" + ], + "redis_cluster_name": "test-cluster" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + From 03e41612bee434fce3ec987208cef5462b955a59 Mon Sep 17 00:00:00 2001 From: Sihyeon Jang Date: Tue, 18 Nov 2025 14:56:55 +0900 Subject: [PATCH 3/4] feat: add limit-count sliding window for redis-cluster with tests Signed-off-by: Sihyeon Jang --- .../limit-count/limit-count-redis-cluster.lua | 71 ++++++- t/plugin/limit-count-redis-cluster-sliding.t | 188 ++++++++++++++++++ 2 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 t/plugin/limit-count-redis-cluster-sliding.t diff --git a/apisix/plugins/limit-count/limit-count-redis-cluster.lua b/apisix/plugins/limit-count/limit-count-redis-cluster.lua index be7029b667ce..fc470627e4c2 100644 --- a/apisix/plugins/limit-count/limit-count-redis-cluster.lua +++ b/apisix/plugins/limit-count/limit-count-redis-cluster.lua @@ -20,7 +20,7 @@ local core = require("apisix.core") local setmetatable = setmetatable local tostring = tostring -local _M = {} +local _M = {version = 0.2} local mt = { @@ -28,7 +28,7 @@ local mt = { } -local script = core.string.compress_script([=[ +local script_fixed = core.string.compress_script([=[ assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1") local ttl = redis.call('ttl', KEYS[1]) if ttl < 0 then @@ -39,7 +39,54 @@ local script = core.string.compress_script([=[ ]=]) -function _M.new(plugin_name, limit, window, conf) +local script_sliding = core.string.compress_script([=[ + assert(tonumber(ARGV[3]) >= 1, "cost must be at least 1") + + local now = tonumber(ARGV[1]) + local window = tonumber(ARGV[2]) + local limit = tonumber(ARGV[3]) + local cost = tonumber(ARGV[4]) + + local window_start = now - window + + redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start) + + local current = redis.call('ZCARD', KEYS[1]) + + if current + cost > limit then + local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') + local reset = 0 + if #earliest == 2 then + reset = earliest[2] + window - now + if reset < 0 then + reset = 0 + end + end + return {-1, reset} + end + + for i = 1, cost do + redis.call('ZADD', KEYS[1], now, now .. ':' .. i) + end + + redis.call('PEXPIRE', KEYS[1], window) + + local remaining = limit - (current + cost) + + local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') + local reset = 0 + if #earliest == 2 then + reset = earliest[2] + window - now + if reset < 0 then + reset = 0 + end + end + + return {remaining, reset} +]=]) + + +function _M.new(plugin_name, limit, window, window_type, conf) local red_cli, err = redis_cluster.new(conf, "plugin-limit-count-redis-cluster-slot-lock") if not red_cli then return nil, err @@ -48,6 +95,7 @@ function _M.new(plugin_name, limit, window, conf) local self = { limit = limit, window = window, + window_type = window_type or "fixed", conf = conf, plugin_name = plugin_name, red_cli = red_cli, @@ -59,12 +107,23 @@ end function _M.incoming(self, key, cost) local red = self.red_cli - local limit = self.limit - local window = self.window key = self.plugin_name .. tostring(key) local ttl = 0 - local res, err = red:eval(script, 1, key, limit, window, cost or 1) + local limit = self.limit + local c = cost or 1 + local res + + if self.window_type == "sliding" then + local now = ngx.now() * 1000 + local window = self.window * 1000 + + res, err = red:eval(script_sliding, 1, key, now, window, limit, c) + else + local window = self.window + + res, err = red:eval(script_fixed, 1, key, limit, window, c) + end if err then return nil, err, ttl diff --git a/t/plugin/limit-count-redis-cluster-sliding.t b/t/plugin/limit-count-redis-cluster-sliding.t new file mode 100644 index 000000000000..66e07125c922 --- /dev/null +++ b/t/plugin/limit-count-redis-cluster-sliding.t @@ -0,0 +1,188 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: redis-cluster policy with sliding window - basic N per window +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "limit-count": { + "count": 2, + "time_window": 2, + "window_type": "sliding", + "rejected_code": 503, + "key": "remote_addr", + "policy": "redis-cluster", + "redis_cluster_nodes": [ + "127.0.0.1:5000", + "127.0.0.1:5001" + ], + "redis_cluster_name": "redis-cluster-1" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + +=== TEST 2: redis-cluster policy with sliding window - enforce N per window +--- pipelined_requests eval +["GET /hello", "GET /hello", "GET /hello"] +--- error_code eval +[200, 200, 503] + + +=== TEST 3: redis-cluster policy with sliding window - remaining header on reject +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local ress = {} + + -- ensure previous windows are expired before starting this test + ngx.sleep(2.2) + + -- first request: allowed, remaining should be 1 + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, {res.status, res.headers["X-RateLimit-Remaining"]}) + end + + -- second request: allowed, remaining should be 0 + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, {res.status, res.headers["X-RateLimit-Remaining"]}) + end + + -- third request: rejected, remaining header should stay at 0 + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(ress, {res.status, res.headers["X-RateLimit-Remaining"]}) + end + + ngx.say(json.encode(ress)) + } + } +--- response_body +[[200,"1"],[200,"0"],[503,"0"]] + + +=== TEST 4: redis-cluster policy with sliding window - allow after window passes +--- config + location /t { + content_by_lua_block { + local json = require "t.toolkit.json" + local http = require "resty.http" + local uri = "http://127.0.0.1:" .. ngx.var.server_port + .. "/hello" + local codes = {} + + -- ensure previous windows are expired before starting this test + ngx.sleep(2.2) + + -- consume full quota + for i = 1, 2 do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(codes, res.status) + end + + -- wait longer than the sliding window (2s) + ngx.sleep(2.2) + + -- should be allowed again after window has passed + do + local httpc = http.new() + local res, err = httpc:request_uri(uri) + if not res then + ngx.say(err) + return + end + table.insert(codes, res.status) + end + + ngx.say(json.encode(codes)) + } + } +--- response_body +[200,200,200] + + From 8e6f1622f1587fa9eb2cd01f5cc83bbff0601d75 Mon Sep 17 00:00:00 2001 From: Sihyeon Jang Date: Tue, 18 Nov 2025 14:57:15 +0900 Subject: [PATCH 4/4] chore: update docs about sliding window Signed-off-by: Sihyeon Jang --- docs/en/latest/plugins/limit-count.md | 37 +++++++++++++++++++++++++-- docs/zh/latest/plugins/limit-count.md | 37 +++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/docs/en/latest/plugins/limit-count.md b/docs/en/latest/plugins/limit-count.md index a5edbc0f2af6..1ca8e23b671a 100644 --- a/docs/en/latest/plugins/limit-count.md +++ b/docs/en/latest/plugins/limit-count.md @@ -4,7 +4,7 @@ keywords: - Apache APISIX - API Gateway - Limit Count -description: The limit-count plugin uses a fixed window algorithm to limit the rate of requests by the number of requests within a given time interval. Requests exceeding the configured quota will be rejected. +description: The limit-count plugin limits the rate of requests by the number of requests within a given time interval. It supports both fixed window and sliding window behaviors. Requests exceeding the configured quota will be rejected. ---