catlabinteractive / neuron
Very light php framework. Should probably not be used.
Requires
- php: >=7.4
- ext-gettext: *
Requires (Dev)
- phpunit/phpunit: ^9.5||^10.0||^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v3.2.1
- v3.2.0
- v3.1.6
- v3.1.5
- v3.1.4
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.17
- v3.0.16
- v3.0.15
- v3.0.14
- v3.0.13
- v3.0.12
- v3.0.11
- v3.0.10
- v3.0.9
- v3.0.8
- v3.0.7
- v3.0.6
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.0.30
- v2.0.29
- v2.0.28
- v2.0.27
- v2.0.26
- v2.0.25
- v2.0.24
- v2.0.23
- v2.0.22
- v2.0.21
- v2.0.20
- v2.0.19
- v2.0.18
- v2.0.17
- v2.0.16
- v2.0.15
- v2.0.14
- v2.0.13
- v2.0.12
- v2.0.11
- v2.0.10
- v2.0.9
- v2.0.8
- 2.0.7
- 2.0.6
- 2.0.5
- 2.0.4
- 2.0.3
- 2.0.2
- 2.0.1
- 2.0.0
- 1.0.x-dev
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.00
- 0.9.0
- 0.2.6
- v0.2.5
- dev-release/3.0.17
- dev-copilot/fix-library-install-requirements
- dev-copilot/add-php-7-4-support
- dev-copilot/fix-input-validation-bug
- dev-version1
This package is auto-updated.
Last update: 2026-09-10 09:00:39 UTC
README
A very light php framework. Will probably not help you much.
Rate limiting
Neuron\RateLimit\RateLimiter is a fixed-window counter with weighted
attempts. The cap check happens inside one conditional UPDATE, so bursts
serialise on the row lock and a refused attempt never consumes budget.
use Neuron\RateLimit\DatabaseStore; use Neuron\RateLimit\RateLimiter; $limiter = new RateLimiter (new DatabaseStore ()); // table neuron_rate_limits if (!$limiter->attempt ('user:export:' . $userId, 100, 3600, $itemCount)) { header ('Retry-After: ' . $limiter->retryAfter (3600)); // 429 } $limiter->cleanup (); // from a daily cron: drops windows older than a day
The application owns the table. Default name neuron_rate_limits
(pass another name to DatabaseStore; it must match /^[A-Za-z0-9_]+$/
and is quoted with backticks in every statement):
CREATE TABLE neuron_rate_limits ( id int unsigned NOT NULL AUTO_INCREMENT, rl_key varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, window_start int unsigned NOT NULL, hits int unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY neuron_rate_limits_key_window (rl_key, window_start), KEY neuron_rate_limits_window (window_start) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
rl_key uses a binary (case-sensitive) collation so keys match exactly;
a case-insensitive collation would merge keys that differ only by case
into the same bucket. neuron_rate_limits_window keeps the cleanup
DELETE (which scans by window_start) indexed. A table with
PRIMARY KEY (rl_key, window_start) and no id works too.
A few contract details worth knowing:
- Key length.
rl_keyisvarchar(128).INSERT IGNOREsilently truncates a longer key to fit instead of erroring, so two distinct keys that only differ past the 128th character would share a bucket. Keep keys short, or hash the long/variable part before using it as a key. max < 1refuses every attempt. There's no special-casing: a cap below 1 can never be satisfied. If0should mean "disabled" (skip rate limiting entirely) in your application, guard for that case before callingattempt().remaining()is a non-atomic read. It issues a plainSELECTwith no lock, so its result can be stale relative to a concurrentattempt()against the same key and window.- Don't call
attempt()inside a long-running explicit transaction. A rollback refunds the budget theINSERT/UPDATEcharged, and holding that row's locks open for the rest of a long transaction can deadlock against other writers hitting the same key. window_startis an unsigned 32-bit epoch bucket (seconds since the epoch, rounded down to a window boundary), matching the column'sint unsignedtype above.