Skip to main content

8 posts tagged with "rate-limiting"

View All Tags

· 3 min read
Josh Twist

I recently got to interview Tom Carden, Head of Engineering at Rewiring America, where he spoke about how they deployed Zuplo’s API Management platform to accelerate their API program.

Rewiring America, a nonprofit focused on electrifying homes, businesses, and communities in the US, required a solution for their IRA Savings Calculator API. “The IRA is the Inflation Reduction Act here in the US” explained Tom, “that puts a bunch of tax credits and rebates in the hands of everyday consumers to help them make a choice for cleaner, safer, and just plain better electric appliances in their home. Forty-two percent of US energy-related emissions come from our homes and we can dramatically reduce that by upgrading the machines we use daily.”

The initial calculator was built as a client-side app but then started receiving requests for an API so that folks could host the functionality on their own website.

Being an experienced Engineering Manager, Tom knew that there was a lot of potential “wheel reinvention” ahead to start a new API program — requiring API authentication, rate-limiting to prevent noisy neighbor problems and abuse, and developer documentation with integrated key management; we call these the three pillars of an API program.

Tom spent some time evaluating different off-the-shelf options before selecting Zuplo to help them get their API to market quicker. “I think I was originally evaluating Zuplo as a documentation host. But once I understood that it was a gateway that could deal with API key management, validation, rate limiting, and also had a long tail of programmable plugins; I was pretty much sold at that point but went off to do my due diligence on alternatives nonetheless.”

Zuplo helped Tom and his team move faster than the alternatives thanks to the focus on developer experience with real-time feedback (press save, and your changes are live to test) and offered a gitops integration with GitHub, meaning they could deploy multiple environments to the edge in seconds.

Tom also described the value of using Zuplo versus building a solution in-house, saving weeks of engineering time and avoiding ongoing maintenance costs. “We could bring this stuff in-house and get something good enough. But I look at that as opportunity costs, we're not solving the problems that are unique to our organization. We're spending time reinventing the wheel. So I'd rather buy.”

With Zuplo's help, Rewiring America can focus on its mission to help Americans upgrade the machines they depend on and avoid reinventing the wheel when it comes to offering a great developer experience to users of their API.

· 3 min read
Josh Twist

If you have used React in the last few years you can't have avoided the useEffect react hook. It's the Batman to your Robin, the Han Solo to your Chewbacca, of the React world.

If you're unfamiliar, the useEffect hook in React is used to handle side-effects in functional components. It allows developers to perform operations such as fetching data from APIs, subscribing to events, and setting up timers. The hook runs after every render or change to dependent property, and provides a way to manage stateful logic in a relatively declarative way.

And therein lies the terror - a dependent property. It's all too easy to create a loop in React, where your useEffect causes a chain of changes that updates the dependent property which triggers your useEffect call, which causes a chain of changes that updates the dependent property which triggers your useEffect call, which causes a chain of changes that updates teh dep... you get the idea.

Here's a simple example

import React, { useState, useEffect } from "react";

function ExampleComponent() {
const [data, setData] = useState([]);

useEffect(() => {
fetch("/api/data")
.then((response) => response.json())
.then((result) => setData(result))
.catch((error) => console.log(error));
}, [data]);

return (
<div>
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

export default ExampleComponent;

Note that the useEffect call declares a dependency on data but also updates that variable with setData. In most cases, these bugs are not so easy to spot and the loop is hidden by many different calls and a cascade of changes making it much harder to debug.

And notice, in this example we are invoking an API at /api/data. We just made a distributed DDoS machine. If you're lucky, this condition will always happen like in the sample above and you'll notice it immediately. If, as happened to me recently, the loop only happens when the app is in a very specific state nested in a bunch of if statements, it might make it to production. At that point, it only takes a small percentage of your user base to experience this bug for you to bring down a full DDoS on your API.

The reality is that you rarely need rate-limiting to protect from your enemies. It's nearly always your partners or in-house development team that will accidentally take your backend down. Unfortunately the nature of React and useEffect makes this more likely.

The good news is it's easy to protect yourself. You can help prevent performance degradation, reduce costs, and improve the overall user experience of your application by implementing a rate-limiting on your API. You can use an open source package like express-rate-limit or use a SaaS service like Zuplo which is fully-managed, multi-cloud and distributed (over 200 data centers worldwide) - see how to get started with rate-limiting using zuplo - you can start for free.

· One min read
Josh Twist

UPDATE See this post for a detailed walkthrough Shipping a ChatGPT plugin in record time

OpenAI recently announced Chat Plugins for Chat GPT.

To make a plugin you just need an API with an accompanying OpenAPI definition that the Chat GPT. The plugin engine is especially impressive - don't take my word for it; here's Mitchell Hashimoto, Founder of HashiCorp:

Tweet from Mitchell Hashimoto

See tweet here

If you already have an API and are excited to make it into a Chat GPT plugin there's a few things you'll need to do

  • Authentication - support auth with an API Key or OAuth2 client-id and secret.
  • OpenAPI - offer an OpenAPI definition so GPT can understand your API
  • Rate-limiting - add rate-limiting to your API (with retry-after) so OpenAI don't over-consume your resources

Fortunately, Zuplo is here to help. Zuplo is an API Gateway that natively supports OpenAPI (you can generate it using our route designer or import your own). What's more we make it easy to support API key authentication and rate-limiting, making it the fastest and easiest way for you to surface your plugin to OpenAI.

Get started for free, sign-up at https://portal.zuplo.com.

· 5 min read
Josh Twist

OpenAI is all the rage now and developers are rushing to leverage this technology in their apps and SaaS products. But such is the demand for this stuff, you might need to think about how you protect yourself from abuse - here's a post from a colleague I saw on LinkedIn recently:

LinkedIn comment

You can use a Zuplo gateway to store your API keys and enforce a number of layers of protection to discourage abuse of your OpenAI API keys.

How it works

Zuplo Gateway for OpenAI

Zuplo allows you to easily perform authentication translation, that is, change the authentication method your clients use. For example you might require your clients to use

  • JWT tokens
  • API Keys issued to each of your customers (different to your OpenAI key, so you can identify each individual customer)
  • Anonymously in a web browser — but ensure the call is coming from the right origin, enforce CORS and rate-limit by IP etc.

Setting up Zuplo to send the API Key

This is a CURL command to call the OpenAI API directly, note the need for an API KEY

curl https://api.openai.com/v1/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY_HERE' \
-d '{
"model": "babbage",
"prompt": "Say this is a test",
"max_tokens": 7,
"temperature": 0
}'

To get started we'll create a simple Zuplo gateway that removes the need to specify the API key.

Create a new project and add a route:

  • Summary: My OpenAI Completions
  • Path: /v1/my-completions
  • Method: POST
  • CORS: No CORS
  • Handler: URL Rewrite - https://api.openai.com/v1/completions

Next, we need to add a policy that will set the authorization header when calling OpenAI. Open the Policies section and click Add Policy.

Add or Set Request Headers

Choose the Add or Set Request Headers policy. Set the policy configuration as follows

{
"export": "SetHeadersInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"headers": [
{
"name": "authorization",
"value": "Bearer $env(OPEN_AI_API_KEY)",
"overwrite": true
}
]
}
}

Note that we will read the API Key from a secure secret stored as an environment variable - go setup your OPEN_AI_API_KEY env var.

Save your changes, and we're ready.

Take the above curl command and remove the authorization header and change the URL to your project URL:

curl https://open-ai-main-298dc8d.d2.zuplo.dev/v1/my-completions \
-H 'Content-Type: application/json' \
-d '{
"model": "babbage",
"prompt": "Say this is a test",
"max_tokens": 7,
"temperature": 0
}'

Look no API key 👏 - but your request should work fine as Zuplo will add the key on the way out.

Securing Zuplo

You now have several choices to secure Zuplo.

  1. Require your users to login (with a service like Auth0) and then use an Auth0 JWT with Zuplo.
  2. Issue API Keys to all your users using Zuplo's API Key Service.
  3. Host anonymously but add additional safe guards, including requiring a specific Origin and strict CORS using custom CORS policies.

Make sure to add rate limiting - based on user or maybe IP (for anonymous use case).

Event Streaming (data-only server-sent events)

OpenAI supports event streaming, this is easy to get working with Zuplo and works out of the box. You can try this by adding a stream: true property to your POST to OpenAI:

curl https://open-ai-main-298dc8d.d2.zuplo.dev/v1/my-completions \
-H 'Content-Type: application/json' \
-d '{
"model": "babbage",
"prompt": "Say this is a test",
"max_tokens": 7,
"temperature": 0,
"stream": true
}'

However, what if you want to support EventSource in the browser? That is easy to accomplish with Zuplo also by taking the incoming GET request created by EventSource and translating it into a POST request, with the appropriate headers and body inside Zuplo.

tip

Event streaming doesn't work fully on 'working-copy' but works great on your 'edge deployments'. Read more about environments to promote to an edge deployment.

Create a new route:

  • Summary: My OpenAI Completions for Browser Event Source
  • Path: /v1/my-browser-completions
  • Method: GET
  • CORS: Anything Goes
  • Handler: URL Rewrite - https://api.openai.com/v1/completions

Add the following policies

  • Reuse your Set Header policy that sets the authorization key above.
  • Add a Change Method policy to update the request to be a POST
  • Add another Set Header policy to set the content-type header to application/json
  • Finally, add a Set Body policy with the following configuration.

Policies

{
"export": "SetBodyInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"body": "{ \"model\": \"babbage\", \"prompt\": \"Say this is a test\", \"max_tokens\": 7, \"temperature\": 0, \"stream\": true }"
}
}

You can now use an EventSource in a browser and call Zuplo as follows

const evtSource = new EventSource(
"open-ai-main-298dc8d.d2.zuplo.dev/v1/my-browser-completions"
);

evtSource.onmessage = (evt) => {
if (evt.data === "[DONE]") {
console.log("end of event stream...");
return;
}
console.log(JSON.parse(evt.data));
};

You could also make the POST body dynamic, based on a querystring in the EventSource - you would then read the querystring values in a custom policy and set the body based on values in the querystring (you would no longer need the Set Body policy in this case).

The custom code (inbound policy) might look like this

import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function (
request: ZuploRequest,
context: ZuploContext,
options: never,
policyName: string
) {
const prompt = request.query.prompt;

// perform any appropriate validation on `prompt` here

const data = {
model: "babbage",
prompt, // pass the query value in here
max_tokens: 7,
temperature: 0,
};

return new ZuploRequest(request, {
body: JSON.stringify(data),
});
}

· 2 min read
Josh Twist

We just published a new video showing how you can add smart routing, behind a single common API for multiple backends, in 1 page of TypeScript. Metadata is loaded from an external service (in this case, Xata but you could use Supabase, Mongo etc).

Here's the code used in the demonstration:

import {
ZuploContext,
ZuploRequest,
environment,
ZoneCache,
} from "@zuplo/runtime";

interface RouteInfo {
customerId: string;
primaryUrl: string;
secondaryUrl?: string;
}

const CACHE_KEY = "ROUTE_RECORDS";
const CACHE_NAME = "ROUTE_INFO";

async function loadRouteInfoFromApi(context: ZuploContext) {
const cache = new ZoneCache(CACHE_NAME, context);

const records = await cache.get(CACHE_KEY);

if (!records) {
const options = {
method: "POST",
headers: {
Authorization: `Bearer ${environment.XATA_API_KEY}`,
"Content-Type": "application/json",
},
body: '{"page":{"size":15}}',
};

const response = await fetch(
"https://YOUR-XATA-URL.xata.sh/db/test:main/tables/routing/query",
options
);

const data = await response.json();
cache.put(CACHE_KEY, data.records, 300); // 5 minutes

context.log.info("RouteInfo loaded from API");
return data.records;
}

context.log.info("RouteInfo loaded from Cache");
return records;
}

export default async function (request: ZuploRequest, context: ZuploContext) {
const customerId = request.user.data.customerId;

const routing = await loadRouteInfoFromApi(context);

const routeInfo = routing.find((r) => r.customerId === customerId);

if (!routeInfo) {
return new Response(`No route found for customer '${customerId}'`, {
status: 404,
});
}

const response = await fetch(routeInfo.primaryUrl);
if (response.status !== 200 && routeInfo.secondaryUrl) {
context.log.info(
`First request failed, trying secondary (${response.status})`
);
const response2 = await fetch(routeInfo.secondaryUrl);
return response2;
}

return response;
}

Got questions or feedback? Join us on Discord.

· 2 min read
Josh Twist

Your supabase backend is often exposed to the public, anybody can sign in create an account and work with data. This can be a problem, if you get a malicious or clumsy user that is hitting your service too hard. That's where you need rate-limits, a way of making sure a single user doesn't starve others of resources (or cost you too much $).

With Zuplo, you can add user-based rate-limiting to a supabase backend in a couple of minutes. There is a video tutorial version of this guide here: YouTube: Per-user rate limit your supabase backend.

Best of all, the only code you'll need to change in your client is the URL of the supabase service (because traffic will now go via Zuplo).

Here are the steps

1/ Create a new project in Zuplo (get a free account at portal.zuplo.com).

2/ Add a route to your new project. Set the following properties

  • path: /(.*) - this is wildcard route that will match all paths
  • methods: all - select all methods in the dropdown
  • CORS: anything goes - this is easiest, but you can set stricter policies
  • URL Rewrite: <https://your-supabase-domain>${pathname} - make sure to add your supabase URL, e.g. https://rxodffgalrhwpvjugcio.supabase.co${pathname}

3/ Add a policy to the request pipeline - choose the supabase-jwt-auth policy. Remove the required claims from the JSON template.

{
"export": "SupabaseJwtInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"secret": "$env(SUPABASE_JWT_SECRET)",
"allowUnauthenticatedRequests": false
}
}

4/ Create an environment variable called SUPABASE_JWT_SECRET (this is in Settings > Environment Variables). Paste in the JWT Secret from supabase (available in Settings > API).

Environment Variable

5/ Add a rate-limiting policy at the end of the request pipeline. Configure it to be a user mode rate limit, suggest 2 requests per minute for demo purposes.

Rate Limit Policy

{
"export": "RateLimitInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"rateLimitBy": "user",
"requestsAllowed": 2,
"timeWindowMinutes": 1
}
}

6/ Get the URL for your gateway by going to Getting Started tab and copying the gateway URL. Replace the Supabase URL in your client and boom 💥!

Getting Started

You now have a rate-limit protected supabase backend. Stay tuned for a subsequent tutorial where we'll show how to ensure folks have to come via Zuplo to call your Supabase backend.

Got questions or feedback, join us on Discord.

· 2 min read
Josh Twist

One of the best things about Zuplo is it's programmable nature. That combined with our approach to making policies composable means you can do some amazing things with them, like our rate-limiter. In this video we show how you can have the rate-limiter interact with external services and data. Here we use supabase as a data-source for the limits.

Here's the key code from the sample

import {
ZuploContext,
ZuploRequest,
ZoneCache,
CustomRateLimitDetails,
} from "@zuplo/runtime";
import { createClient } from "@supabase/supabase-js";

const fallBackLimits = {
requestsAllowed: 10000,
timeWindowMinutes: 1,
};

const CACHE_NAME = "rate-limit-cache";
const CACHE_KEY = "rate-limit-data";

export async function getRateLimit(
request: ZuploRequest,
context: ZuploContext,
policyName: string
) {
const limitResponse: CustomRateLimitDetails = {
key: request.user.sub,
...fallBackLimits,
};

const userGroup = request.user.data.rateLimitGroup;
const cache = new ZoneCache(CACHE_NAME, context);

const cached: any = await cache.get(CACHE_KEY);

if (cached) {
context.log.debug("cache hit");
const item = cached.find((row) => row.userGroup === userGroup);
limitResponse.requestsAllowed = item.reqPerMinute;
return limitResponse;
}

context.log.debug("cache miss");
const supabase = createClient(
"https://YOUR_SUPABASE_URL.supabase.co",
"YOUR_SUPABASE_TOKEN"
);
const { data, error } = await supabase.from("rate-limits").select();

if (error) {
context.log.error(`Error reading data from supabase`, error);
// return fallback rate-limit - don't want API downtime
// if this dependency is down.
return limitResponse;
}

const item = data.find((row) => row.userGroup === userGroup);

if (!item) {
context.log.warn(`No row rateLimitId '${userGroup}' found, using fallback`);
// return fallback
return limitResponse;
}

void cache.put(CACHE_KEY, data, 10);

limitResponse.requestsAllowed = item.reqPerMinute;
return limitResponse;
}

You could make this even higher performance by having the cache have a longer expiry, but periodically reloading the data from supabase asynchronously and pushing the results back into the cache; something like an SWR (stale, while revalidate) approach.

Get started with Zuplo for free today: Sign Up Free

See also:

Shipping a public API backed by Supabase

API Authentication using Supabase JWT tokens

· 2 min read
Josh Twist

AKA Why you need rate-limiting, and ideally Dynamic Rate Limiting.

Length: 2 minutes

Before launching any API program you need to think about protection. Many API developers don't think they need to worry about rate-limiting because they aren't a target for hackers. That's probably true in the early days, but it isn't the hackers that are going to DoS you; it's your prized customers.

The most common type of API abuse isn't malicious, it's accidental. It's a misplaced for-loop in your partners code that takes you down. This happens often.

So ideally your API has some per-user rate limits. This is super easy with Zuplo.

However, in reality this isn't enough. You should probably have different rate-limits for different customers. For example, your free customers should have a more restrictive policy while your paid, or premium customers a more lenient one.

Guess what? That's also easy with Zuplo because you can write custom code that tells the rate-limit policy how to work. In the video above we show how you can modify the policy limits based on customer type (in this example, stored in the API Key metadata, but could be based on JWT claim, or even a cache-able DB lookup if so required).

Here's the code from request-limit-lookup.ts file in the video:

import { CustomRateLimitDetails, ZuploRequest } from "@zuplo/runtime";

const requestsPerMinute = {
premium: 3,
free: 1,
};

export default function (request: ZuploRequest): CustomRateLimitDetails {
const customerType = request.user.data.customerType;
const reqsPerMinute = requestsPerMinute[customerType];

const rateLimitConfig = {
// The key tells the rate limiter how to correlate different requests
key: request.user.sub,
requestsAllowed: reqsPerMinute,
timeWindowMinutes: 1,
};

return rateLimitConfig;
}

And the config for the rate-limit policy

{
"export": "RateLimitInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"rateLimitBy": "function",
"identifier": {
"module": "$import(./modules/request-limit-lookup)",
"export": "default"
}
}
}

Stay safe out there folks!