I am using Firebase to develop an app that uses Cloud Functions as a REST API internally. My question is, is there an easy way to implement per-IP/per-user rate-limiting similar to what slack uses, except on a per-IP and per-user basis, rather than per-app (since it's all one app). Optional support for small bursts is preferable as well.
Example code (see the // TODO:
comments):
exports.myCoolFunction = functions.https.onRequest((req, res) => {
// TODO: implement IP rate-limiting here
unpackToken(req).then((token) => { // unpackToken resolves with a response similar to verifyIdToken based on the "Authorization" header contents
// TODO: implement user-based rate-limiting here (based on token.uid)
if (!req.body) return res.status(400).end();
if (typeof req.body.name !== "string") return res.status(400).end();
if (typeof req.body.user !== "string") return res.status(400).end();
// more input sanitization and function logic here
return res.status(501).end(); // fallback in all requests, do not remove
}).catch(() => res.status(403).end());
});
I want to terminate the request simply with a 529 Too Many Requests
status code if the rate limit is exceeded. This is to prevent application errors from flooding the network and to prevent abuse of the REST API.
This should take into account Firebase spinning up/down server instances and having multiple instances running simultaneously.
I am also using a Firestore database and can use the legacy real-time database if necessary.
While not a hard limit, if you sustain more than 1,000 writes per second, your write activity may be rate-limited. 256 MB from the REST API; 16 MB from the SDKs. The total data in each write operation should be less than 256 MB. Multi-path updates are subject to the same size limitation.
The function itself takes about 400ms, so that's alright. But sometimes the functions take a very long time (around 8 seconds), while the entry was already added to the queue.
During GET operations you have the limits: Maximum number of exists(), get(), and getAfter() calls per request: 10 for single-document requests and query requests. 20 for multi-document reads, transactions, and batched writes. The previous limit of 10 also applies to each operation.
Doing this on a per-user basis sounds fairly straightforward:
functions-samples
repo.admin.database().ref(`/userCalls/$uid`).push(ServerValue.TIMESTAMP)
.admin.database().ref(`/userCalls/$uid`).orderByKey().startAt(Date.now()-60000)
.I'm not sure if the IP address of the caller is passed to Cloud Functions. If it is, you can do the same logic for the IP address. If it isn't passed, it'll be hard to rate limit in a way that can't be easily spoofed.
I made a library for rate-limiting calls to firebase functions: firebase-functions-rate-limiter The library uses realtimeDB or firestore (configurable) as a backend. It stores data, in a similar approach that Frank described, but is more economical. Instead of using a collection, it uses a single document with array per each qualifier (eg. a user id). That means there is only a single read for an exceeded call, and a read-write for an allowed call.
$ npm i --save firebase-functions-rate-limiter
Here is an example:
import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
import { FirebaseFunctionsRateLimiter } from "firebase-functions-rate-limiter";
admin.initializeApp(functions.config().firebase);
const database = admin.database();
const limiter = FirebaseFunctionsRateLimiter.withRealtimeDbBackend(
{
name: "rate_limiter_collection",
maxCalls: 2,
periodSeconds: 15,
},
database,
);
exports.testRateLimiter =
functions.https.onRequest(async (req, res) => {
await limiter.rejectOnQuotaExceeded(); // will throw HttpsException with proper warning
res.send("Function called");
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With