firebase-function-rate-limiter
v1.2.0
Published
Rate limiting middleware for Firebase Functions with Firestore, Realtime Database, and memory stores.
Maintainers
Readme
Firebase Function Rate Limiter
Rate limiting for Firebase Functions using Firestore, Firebase Realtime Database, or local memory. It provides Express middleware for HTTP functions and a direct check() API for callable functions.
Features
- Atomic counters shared by function instances
- Firestore and Realtime Database backends
- Express/Firebase
onRequestmiddleware - Direct checks for callable functions
- Rate-limit and retry headers
- Custom keys, skip rules, messages, and rejection handlers
- TypeScript declarations
Installation
npm install firebase-function-rate-limiter firebase-adminNode.js 18 or newer is required. The package uses CommonJS:
const {
rateLimit,
FirestoreStore,
RealtimeDatabaseStore,
MemoryStore,
} = require('firebase-function-rate-limiter');Realtime Database quick start
const { initializeApp } = require('firebase-admin/app');
const { getDatabase } = require('firebase-admin/database');
const { onRequest } = require('firebase-functions/v2/https');
const express = require('express');
const { rateLimit, RealtimeDatabaseStore } = require('firebase-function-rate-limiter');
initializeApp();
const app = express();
app.use(rateLimit({
store: new RealtimeDatabaseStore({
database: getDatabase(),
path: '_functionRateLimits',
}),
limit: 60,
windowMs: 60_000,
keyGenerator: (req) => `${req.user.uid}/hello`,
}));
app.get('/hello', (req, res) => res.json({ message: 'Hello!' }));
exports.api = onRequest(app);For a non-default RTDB instance, supply its URL:
initializeApp({ databaseURL: 'https://YOUR_PROJECT_ID-default-rtdb.firebaseio.com' });Firestore quick start
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
const { onRequest } = require('firebase-functions/v2/https');
const express = require('express');
const { rateLimit, FirestoreStore } = require('firebase-function-rate-limiter');
initializeApp();
const app = express();
app.use(rateLimit({
store: new FirestoreStore({
firestore: getFirestore(),
collection: '_functionRateLimits',
}),
limit: 60,
windowMs: 60_000,
keyGenerator: (req) => `${req.user.uid}/hello`,
}));
app.get('/hello', (req, res) => res.json({ message: 'Hello!' }));
exports.api = onRequest(app);Callable functions
Use check() without Express. Convert RateLimitError into a Firebase HttpsError for callable clients:
const { onCall, HttpsError } = require('firebase-functions/v2/https');
const { getDatabase } = require('firebase-admin/database');
const { rateLimit, RealtimeDatabaseStore, RateLimitError } = require('firebase-function-rate-limiter');
const limiter = rateLimit({
store: new RealtimeDatabaseStore({ database: getDatabase() }),
limit: 10,
windowMs: 60_000,
});
exports.expensiveTask = onCall(async (request) => {
if (!request.auth) throw new HttpsError('unauthenticated', 'Sign in first.');
const key = `${request.auth.uid}auth/expensiveTask`;
try {
await limiter.check(key);
} catch (error) {
if (error instanceof RateLimitError) {
throw new HttpsError('resource-exhausted', error.message, {
retryAt: error.resetTime.toISOString(),
});
}
throw error;
}
return { ok: true };
});Choosing a store
| Store | Use case | Shared across instances | Persistent |
| --- | --- | --- | --- |
| RealtimeDatabaseStore | Production apps using RTDB | Yes | Yes |
| FirestoreStore | Production apps using Firestore | Yes | Yes |
| MemoryStore | Tests and local emulators | No | No |
Both Firebase stores use transactions. Do not use MemoryStore for a global production limit: every function instance has a separate counter.
Key selection
Supply the complete key through keyGenerator for middleware or limiter.check(key) for direct checks. The library does not hash keys or add role, meter, or function names.
| Caller | Key with meter | Key without meter |
| --- | --- | --- |
| Admin | userid/meter-serial/function-name | userid/function-name |
| Alt | useridalt/meter-serial/function-name | useridalt/function-name |
| Auth | useridauth/meter-serial/function-name | useridauth/function-name |
| Consumer | meter-serial/function-name | — |
userid + alt and userid + auth mean concatenation, for example user123alt/meter456/readMeter.
// Construct the key in your application using verified identity and role data.
const key = [userId + 'auth', meterSerial, 'readMeter'].filter(Boolean).join('/');
await limiter.check(key);RTDB uses the key as a nested path beneath path. Each segment must be a valid RTDB node name (no ., #, $, [, ], or control characters). Firestore stores the key as a single document ID beneath collection, using encodeURIComponent so / becomes %2F. This is reversible encoding, not hashing. For example, user123/meter456/readMeter becomes user123%2Fmeter456%2FreadMeter in Firestore. Caller keys must fit the backend's path and document ID limits.
An optional prefix adds a parent path (prefix/key) before storage or Firestore encoding. Leave it empty to use exactly the formats above. Include the function name to keep independent function counters separate.
If keyGenerator is omitted, the existing UID/IP fallback still applies. The HTTP examples assume authentication middleware has populated req.user.
Migration: These paths differ from the previous hashed IDs, so existing counters will not be reused. Remove old hashed records separately if needed.
Configuration
const limiter = rateLimit({
store,
limit: 20,
windowMs: 5 * 60_000,
keyGenerator: (req) => req.user?.uid || req.ip,
skip: (req) => req.path === '/health',
standardHeaders: true,
message: 'Please wait before trying again.',
onRejected: (req, res, error) => {
res.status(429).json({ error: error.message });
},
});See the complete API reference.
Response headers
RateLimit-Limit: maximum requests in the windowRateLimit-Remaining: requests remainingRateLimit-Reset: Unix timestamp when the window resetsRetry-After: seconds to wait, on rejected requests
Database cleanup
Both database stores write only count and resetAt. RTDB uses milliseconds since the Unix epoch for resetAt; Firestore uses a timestamp. Expired counters reset on their next request. Optional cleanup can use resetAt; no separate expiration field is written.
Security
- Use server-side Admin SDK database instances, not client SDK references.
- Prefer verified Firebase Auth UIDs over IP addresses.
- Trust
X-Forwarded-Foronly behind Firebase/Google's trusted proxy. - Use different
prefixvalues for limits that should not share counters. - Handle database errors in your Express error middleware.
License
MIT
