<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Unkey</title>
    <description>The latest articles on DEV Community by Unkey (unkey).</description>
    <link>https://dev.to/unkey</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9070%2F1d04dc38-9277-4e7c-8c12-885de4f57da2.png</url>
      <title>DEV Community: Unkey</title>
      <link>https://dev.to/unkey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/unkey"/>
    <language>en</language>
    <item>
      <title>Deploy an API with preview environments and request logs</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Fri, 06 Nov 2026 11:37:43 +0000</pubDate>
      <link>https://dev.to/unkey/deploy-an-api-with-preview-environments-and-request-logs-3m5b</link>
      <guid>https://dev.to/unkey/deploy-an-api-with-preview-environments-and-request-logs-3m5b</guid>
      <description>&lt;p&gt;When you ship a public API, you need somewhere to run it and a preview for each pull request. You also need to check who is calling, and you want logs for the day something breaks. We'll set all of that up on Unkey by connecting a GitHub repository. Every push deploys the app, Compute runs it, and API Management issues and checks the keys. Unkey also records each request the app serves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll have at the end
&lt;/h2&gt;

&lt;p&gt;You get a production URL on &lt;code&gt;unkey.app&lt;/code&gt; that updates when you push to your default branch, and a preview URL for every other branch and fork pull request. The gateway checks API keys and rate limits each caller, and you get logs and metrics without adding an SDK.&lt;/p&gt;

&lt;p&gt;You need a Compute plan before the first deploy. The dashboard asks you to &lt;strong&gt;Choose a Compute plan&lt;/strong&gt;, and only a workspace admin can pick one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sample API
&lt;/h2&gt;

&lt;p&gt;The app is &lt;a href="https://github.com/perkinsjr/Demo-for-deploy" rel="noopener noreferrer"&gt;perkinsjr/Demo-for-deploy&lt;/a&gt;, a small Go API built with Gin. It listens on port 8080, which is the port Unkey uses by default, and serves five routes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;What it returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{"status":"healthy"}&lt;/code&gt; and a timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /items&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every item and a count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /items/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One item, or &lt;code&gt;404&lt;/code&gt; with &lt;code&gt;{"error":"Item not found"}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /items&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The item it created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /whoami&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;X-Unkey-Principal&lt;/code&gt; header as JSON, or &lt;code&gt;{"principal":null}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1: Connect the repository and deploy
&lt;/h2&gt;

&lt;p&gt;You can connect and deploy from the dashboard, the API, or the CLI. All three get you the same app.&lt;/p&gt;

&lt;h3&gt;
  
  
  From the dashboard
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Projects&lt;/strong&gt;, choose &lt;strong&gt;Create project&lt;/strong&gt;, and enter a &lt;strong&gt;Project Name&lt;/strong&gt;. Unkey generates the &lt;strong&gt;Slug&lt;/strong&gt; from the name. You'll use that slug whenever you call the API or the CLI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8imu8ouyam5mk4424mpo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8imu8ouyam5mk4424mpo.png" alt="The Create New Project dialog with Project Name and Slug fields" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose &lt;strong&gt;Create app&lt;/strong&gt;, then choose &lt;strong&gt;GitHub repository&lt;/strong&gt; as the source. The first time you do this in a workspace, GitHub asks you to install the Unkey GitHub App and give it access to the repository.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevvddtr7bt4lt2p96d9v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevvddtr7bt4lt2p96d9v.png" alt="Deploy a new app, with GitHub repository as the source" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then pick the repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm6makzkivzl0igdmpov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm6makzkivzl0igdmpov.png" alt="The Import a GitHub repository step listing the account's repositories" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On &lt;strong&gt;Set up your app&lt;/strong&gt;, enter a name and pick the branch. If the app needs a secret, such as a database URL, choose &lt;strong&gt;Edit&lt;/strong&gt; next to environment variables and add it. If you mark it sensitive, it stays write-only. Use the environment dropdown to give Production and Preview separate values.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzwwitsm4atpkfogzj08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzwwitsm4atpkfogzj08.png" alt="The Set up your app step: name api, branch main, automatic build, one instance in four regions" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtr53rvzzpru9vhdnkg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxtr53rvzzpru9vhdnkg9.png" alt="The Environment variables dialog scoped to the Preview environment" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose &lt;strong&gt;Deploy&lt;/strong&gt;. The build log streams while the image builds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2wkv2wmc396gkfhxig75.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2wkv2wmc396gkfhxig75.png" alt="A deployment building from main, with the build log open" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1zbl142dcxgrf2iom9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1zbl142dcxgrf2iom9x.png" alt="The same deployment at Ready, with Show URLs open and one instance in each of four regions" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  From the API
&lt;/h3&gt;

&lt;p&gt;Create a project, then create an app connected to your repository. Unkey reads &lt;code&gt;defaultBranch&lt;/code&gt; from the repository, so you don't need to set it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v2/projects.createProject &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"my-project","slug":"my-project"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v2/apps.createApp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"project":"my-project","name":"api","slug":"api","git":{"repository":"owner/repo"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy the default branch to production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v3/deployments.createDeployment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"project":"my-project","app":"api","environment":"production","git":{"branch":"main"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call returns &lt;code&gt;201&lt;/code&gt; and a &lt;code&gt;deploymentId&lt;/code&gt;. Poll &lt;code&gt;deployments.getDeployment&lt;/code&gt; with that id until &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;ready&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v2/deployments.getDeployment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"deploymentId":"d_xxx"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it's ready, the app is live at &lt;code&gt;https://&amp;lt;project&amp;gt;-&amp;lt;app&amp;gt;-&amp;lt;workspace&amp;gt;.unkey.app&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  From the CLI
&lt;/h3&gt;

&lt;p&gt;Install the CLI and export your root key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; unkey
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your root key&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the same four calls with the CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unkey api projects create-project &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project &lt;span class="nt"&gt;--slug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project
unkey api apps create-app &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api &lt;span class="nt"&gt;--slug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--git&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"repository":"owner/repo"}'&lt;/span&gt;
unkey api deployments create-deployment &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project &lt;span class="nt"&gt;--app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="nt"&gt;--git&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"branch":"main"}'&lt;/span&gt;
unkey api deployments get-deployment &lt;span class="nt"&gt;--deployment-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;d_xxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Every branch and pull request gets a preview
&lt;/h2&gt;

&lt;p&gt;Once the repository is connected, every push to the default branch deploys production. A push to any other branch creates a preview with its own branch domain. Each one shows up in GitHub as a deployment. Unkey also keeps one comment on the pull request updated with a &lt;strong&gt;Visit Preview&lt;/strong&gt; link.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fso3gybooratc7beemxae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fso3gybooratc7beemxae.png" alt="The app's Deployments list filtered to Preview, showing the Add /whoami route preview" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open a preview deployment to see its branch, commit, and environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8osa9qkmvclh1gjtfb0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8osa9qkmvclh1gjtfb0f.png" alt="A preview deployment for Add /whoami route, with its branch, commit, and Preview environment" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A pull request from a fork waits for a project member to approve it, because its code would run with your preview secrets.&lt;/p&gt;

&lt;p&gt;Give the preview environment its own values. Point it at a test database and test-mode payment keys. You can also give it a separate keyspace, so a preview key can't call production.&lt;/p&gt;

&lt;p&gt;Unkey stops previews in these cases, so you don't have to clean them up yourself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;What stops&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New preview deployment on the same branch&lt;/td&gt;
&lt;td&gt;The deployment that was on that branch domain&lt;/td&gt;
&lt;td&gt;1 minute later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview deployment gets no requests&lt;/td&gt;
&lt;td&gt;That deployment&lt;/td&gt;
&lt;td&gt;After 1 hour idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You stop it (&lt;strong&gt;Stop deployment&lt;/strong&gt; or &lt;code&gt;deployments.stopDeployment&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;That deployment&lt;/td&gt;
&lt;td&gt;Right away&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Wake a stopped preview with &lt;strong&gt;Wake deployment&lt;/strong&gt; or &lt;code&gt;deployments.startDeployment&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Require an API key and rate limit each customer
&lt;/h2&gt;

&lt;p&gt;Every Compute app sits behind the gateway. Policies run on the gateway, so it checks each request before the request reaches your code. Start by creating a keyspace in API Management. In the API, a keyspace is called an API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v2/apis.createApi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"my-api"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the &lt;code&gt;apiId&lt;/code&gt; from the response. You need it every time you create a key.&lt;/p&gt;

&lt;p&gt;Now create a key for a customer and attach a rate limit to it. &lt;code&gt;externalId&lt;/code&gt; links the key to an identity. With &lt;code&gt;autoApply&lt;/code&gt; set, every request made with this key counts against the limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v2/keys.createKey &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"apiId":"api_xxx","name":"acme blog demo","externalId":"acme","ratelimits":[{"name":"requests","limit":5,"duration":60000,"autoApply":true}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's 5 requests per minute. It's low enough that you can watch it trip. In production, start closer to 100.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data.key&lt;/code&gt; is the full key, and you only see it in this response. Unkey stores just a hash, so hand the key to the customer now.&lt;/p&gt;

&lt;p&gt;Next, tell the gateway to check keys. Open the app's &lt;strong&gt;Policies&lt;/strong&gt; and choose &lt;strong&gt;Add policy&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give the policy a name and set &lt;strong&gt;Type&lt;/strong&gt; to &lt;strong&gt;Key Auth&lt;/strong&gt;. The rule reads &lt;strong&gt;IF&lt;/strong&gt; Every request &lt;strong&gt;THEN&lt;/strong&gt; Key Auth. Under &lt;strong&gt;Keyspaces&lt;/strong&gt;, pick the keyspace you just created.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm65f505kbj5meh43mql3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm65f505kbj5meh43mql3.png" alt="The Add policy form: name Require an API key, Type Key Auth, rule IF Every request THEN Key Auth, with a keyspace selected" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set &lt;strong&gt;Environment&lt;/strong&gt; to Production. &lt;strong&gt;ON FAIL&lt;/strong&gt; shows what callers get back when the check fails, which is &lt;code&gt;401&lt;/code&gt;, &lt;code&gt;403&lt;/code&gt;, or &lt;code&gt;429&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvrz1u6rkcg2bx9ke5qfd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvrz1u6rkcg2bx9ke5qfd.png" alt="The rest of the Add policy form, with ON FAIL codes 401, 403, and 429 and Environment set to Production" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose &lt;strong&gt;Add policy&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flrjssvfwe6lmdp2us2lo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flrjssvfwe6lmdp2us2lo.png" alt="The Policies list with Require an API key, a Key Auth policy that is on for Production" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You don't need a separate rate limit policy. Once the gateway verifies the key, it also enforces the limit you attached to that key.&lt;/p&gt;

&lt;p&gt;Policies apply on the next deployment, so redeploy production to turn this one on.&lt;/p&gt;

&lt;p&gt;If you'd rather use the API or CLI, you can set the same policy with one command. Both need the keyspace id (&lt;code&gt;ks_xxx&lt;/code&gt;), which is on the keyspace &lt;strong&gt;Settings&lt;/strong&gt; page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unkey api gateway set-policies &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project &lt;span class="nt"&gt;--app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api &lt;span class="nt"&gt;--environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{"name":"Require an API key","enabled":true,"keyauth":{"keyspaces":["ks_xxx"]}}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v2/gateway.setPolicies &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"project":"my-project","app":"api","environment":"production","policies":[{"name":"Require an API key","enabled":true,"keyauth":{"keyspaces":["ks_xxx"]}}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;setPolicies&lt;/code&gt; replaces the environment's whole policy list, so include every policy you want to keep. The root key needs &lt;code&gt;environment.*.set_policies&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the new deployment is live, call the app without a key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://your-app.unkey.app/items
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway answers &lt;code&gt;401&lt;/code&gt;, and the request never reaches your code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"err:frontline:client:missing_credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authentication required. Please provide a valid API key."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now call &lt;code&gt;/whoami&lt;/code&gt; with the key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://your-app.unkey.app/whoami
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response headers show the caller's limit and how many requests they have left.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/2 200
x-ratelimit-limit: 5
x-ratelimit-remaining: 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The body is the &lt;code&gt;X-Unkey-Principal&lt;/code&gt; header that the gateway added. It tells your code who is calling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"identity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"externalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"keyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"key_xxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"keySpaceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ks_xxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme blog demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;subject&lt;/code&gt; is the identity's &lt;code&gt;externalId&lt;/code&gt;, or the key id when the key has no identity. The gateway strips any &lt;code&gt;X-Unkey-*&lt;/code&gt; header the client sends. Compute deployments are only reachable through the gateway, so inside Compute you can trust that Unkey set the header. If you run the same code somewhere else, a caller can forge it, so don't trust it there.&lt;/p&gt;

&lt;p&gt;Once a caller goes over the limit, requests return &lt;code&gt;429&lt;/code&gt; with a &lt;code&gt;retry-after&lt;/code&gt; header that gives the wait in seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/2 429
retry-after: 53
x-ratelimit-limit: 5
x-ratelimit-remaining: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"err:frontline:client:rate_limited"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rate limit exceeded. Please try again later."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preview has its own policy list, so your production policies don't cover it. If a preview shouldn't be public, set policies for it too, or choose &lt;strong&gt;All Environments&lt;/strong&gt; in the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Find and fix a bug
&lt;/h2&gt;

&lt;p&gt;Say a change to the item lookup ships to production, and customers' &lt;code&gt;GET /items/2&lt;/code&gt; calls start failing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://your-app.unkey.app/items/2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"internal server error"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Requests&lt;/strong&gt; for the project and filter &lt;strong&gt;Status&lt;/strong&gt; to 5xx. Every failure is a &lt;code&gt;GET /items/2&lt;/code&gt; that returned &lt;code&gt;500&lt;/code&gt;. Open one to see the deployment, branch, and commit that served it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5fgraaqf7b6foe7cts9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5fgraaqf7b6foe7cts9.png" alt="Requests filtered to 5xx: three GET /items/2 calls returned 500, with one open to show the production deployment, branch, and commit that served it" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Logs&lt;/strong&gt; and search for &lt;code&gt;index out of range&lt;/code&gt;. The app logged &lt;code&gt;error: get item 2: index out of range&lt;/code&gt; on each failed call.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0yxs89s11ymi2clrcbr7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0yxs89s11ymi2clrcbr7.png" alt="Logs searched for index out of range: three ERROR lines, with error: get item 2: index out of range open" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push the fix to a branch. Unkey builds a preview for it. Call the same path on the preview's branch domain with your key.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://your-app-git-your-branch.unkey.app/items/2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sample Item 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Another fake item for testing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-16T14:57:30.985616522Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyz2p4iy6352ex0870cp5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyz2p4iy6352ex0870cp5.png" alt="The app's Deployments list: the Fix item lookup preview at Ready, above the current production deployment, Look up items by index" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Merge the branch. The push to your default branch deploys the fix to production.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The gateway rejects some requests, like ones with a missing key or ones over the rate limit. Those never reach your app, so they don't show up in &lt;strong&gt;Requests&lt;/strong&gt;. You can find those key checks in API Management's verification analytics.&lt;/p&gt;

&lt;p&gt;To query the same data with SQL, call &lt;code&gt;analytics.getGatewayRequests&lt;/code&gt;. The root key needs &lt;code&gt;project.*.read_gateway_requests&lt;/code&gt;. Queries cover the last 7 days.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.unkey.com/v2/analytics.getGatewayRequests &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$UNKEY_ROOT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"query":"SELECT path, count() AS requests FROM gateway_requests_v1 WHERE response_status &amp;gt;= 500 AND time &amp;gt;= toUnixTimestamp64Milli(now64(3) - INTERVAL 1 HOUR) GROUP BY path ORDER BY requests DESC LIMIT 10"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How this compares with GitHub Actions plus Cloud Run, ECS or Kubernetes
&lt;/h2&gt;

&lt;p&gt;The other setups leave more of this to you. Each provider column links to that provider's docs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Unkey&lt;/th&gt;
&lt;th&gt;&lt;a href="https://docs.cloud.google.com/run/docs/deploying" rel="noopener noreferrer"&gt;GitHub Actions + Cloud Run&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/update-service-console-v2.html" rel="noopener noreferrer"&gt;GitHub Actions + ECS&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/" rel="noopener noreferrer"&gt;GitHub Actions + Kubernetes&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deploy on push&lt;/td&gt;
&lt;td&gt;Connect the repo and it's built in&lt;/td&gt;
&lt;td&gt;Workflow runs &lt;code&gt;gcloud run deploy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Workflow registers a task definition and updates the service&lt;/td&gt;
&lt;td&gt;Workflow applies manifests or sets the image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preview per branch or PR&lt;/td&gt;
&lt;td&gt;Built-in, with a branch domain and fork approval&lt;/td&gt;
&lt;td&gt;Workflow deploys a revision with &lt;code&gt;--no-traffic --tag&lt;/code&gt; to get a tagged URL (&lt;a href="https://cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Not covered in the linked docs. You script a service per PR&lt;/td&gt;
&lt;td&gt;Not covered in the linked docs. You script a namespace or release per PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean up previews&lt;/td&gt;
&lt;td&gt;Auto-stop after 1 hour idle, or 1 minute after a newer preview on the branch&lt;/td&gt;
&lt;td&gt;You remove tags or revisions yourself&lt;/td&gt;
&lt;td&gt;Your workflow tears down what it created&lt;/td&gt;
&lt;td&gt;Your workflow tears down what it created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API key auth and per-key rate limits&lt;/td&gt;
&lt;td&gt;Gateway policies backed by API Management keys&lt;/td&gt;
&lt;td&gt;A separate gateway or code in your app&lt;/td&gt;
&lt;td&gt;A separate gateway or code in your app&lt;/td&gt;
&lt;td&gt;An ingress or gateway add-on, or code in your app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request logs&lt;/td&gt;
&lt;td&gt;Built-in, plus SQL via the analytics API&lt;/td&gt;
&lt;td&gt;Cloud Logging&lt;/td&gt;
&lt;td&gt;CloudWatch, if you configure it&lt;/td&gt;
&lt;td&gt;Whatever logging stack you run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Render's &lt;a href="https://render.com/docs/service-previews" rel="noopener noreferrer"&gt;service previews&lt;/a&gt; are the closest built-in match for the preview row. You choose &lt;strong&gt;Manual&lt;/strong&gt; or &lt;strong&gt;Automatic&lt;/strong&gt; pull request previews, and Render deletes a preview when its pull request is merged or closed. Unkey also puts a gateway in front of production and previews, and the request log is queryable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;API Management and Compute are billed separately. You can use either one without the other. Prices below are from &lt;a href="https://www.unkey.com/pricing" rel="noopener noreferrer"&gt;unkey.com/pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Compute has three plans. Each one includes a monthly usage credit equal to the fee. Until December 31, 2026, the first month is discounted. Starter costs $0 the first month and $5 after that, Pro costs $20 and then $25, and Business costs $45 and then $50. On Starter, credit left over from that first month carries to the next bill. Unused credits otherwise do not roll over.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Starter&lt;/th&gt;
&lt;th&gt;Pro&lt;/th&gt;
&lt;th&gt;Business&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly fee&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First month&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;$45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage credit each month&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max per instance&lt;/td&gt;
&lt;td&gt;2 vCPU, 2 GiB&lt;/td&gt;
&lt;td&gt;8 vCPU, 8 GiB&lt;/td&gt;
&lt;td&gt;16 vCPU, 32 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instances per region&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domains&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log retention&lt;/td&gt;
&lt;td&gt;3 days&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;14 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team members&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every plan allows one concurrent build, git push to deploy, and a preview for each pull request. Preview usage is billed at the same rates as production.&lt;/p&gt;

&lt;p&gt;Past the included credit, you pay for the vCPU and memory your app actually uses, the disk you allocate, egress, and active keys checked by a Compute key auth policy. The rates below are rounded.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Meter&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vCPU&lt;/td&gt;
&lt;td&gt;about $0.025 per hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;about $9 per GB-month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk&lt;/td&gt;
&lt;td&gt;about $0.16 per GB-month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress&lt;/td&gt;
&lt;td&gt;$0.05 per GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active keys&lt;/td&gt;
&lt;td&gt;$2 per 1,000 per month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the default instance (0.25 vCPU, 256 MiB) ran flat out all month, you'd pay about $4.50 for CPU and $2.25 for memory, so about $6.75, and the Starter credit covers most of that. Treat that as the most it could cost. A real API almost never keeps CPU and memory at 100 percent around the clock, so your bill will land well under it. You pay for the CPU and memory your app actually uses, not what you configured. Memory your app holds is billed even between requests. CPU used while your app is idle or waiting on I/O is usually tiny, so it costs very little.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spending Caps
&lt;/h3&gt;

&lt;p&gt;You can cap the bill with a monthly budget.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set it in whole dollars under &lt;strong&gt;Settings &amp;gt; Billing&lt;/strong&gt;. Only a workspace admin can change it.&lt;/li&gt;
&lt;li&gt;Admins get an email the first time month-to-date spend crosses 50, 75, and 100 percent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop workloads at the budget&lt;/strong&gt; is off until you turn it on. At 100 percent, running deployments stop and every request gets &lt;code&gt;402&lt;/code&gt; &lt;code&gt;spend_limit_reached&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove the budget, turn the stop option off, or wait for the next month, and the live deployment comes back without a redeploy. Raising the budget resumes compute once spend has stayed under the new budget for two checks in a row.&lt;/li&gt;
&lt;li&gt;Nothing is deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://unkey.com/docs/compute/get-started/deploy-from-github" rel="noopener noreferrer"&gt;Deploy from GitHub&lt;/a&gt; and &lt;a href="https://unkey.com/docs/compute/concepts/production-and-preview" rel="noopener noreferrer"&gt;Production and preview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unkey.com/docs/compute/gateway/policies" rel="noopener noreferrer"&gt;Gateway policies&lt;/a&gt;, &lt;a href="https://unkey.com/docs/compute/gateway/api-key-auth" rel="noopener noreferrer"&gt;API key authentication policy&lt;/a&gt;, &lt;a href="https://unkey.com/docs/compute/gateway/principal" rel="noopener noreferrer"&gt;The principal header&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unkey.com/docs/compute/observe/overview" rel="noopener noreferrer"&gt;Observability&lt;/a&gt; and &lt;a href="https://unkey.com/docs/compute/observe/analytics-api" rel="noopener noreferrer"&gt;Query gateway requests and runtime logs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unkey.com/docs/platform/billing/plans" rel="noopener noreferrer"&gt;Plans&lt;/a&gt; and &lt;a href="https://www.unkey.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Related posts: &lt;a href="https://www.unkey.com/blog/best-serverless-api-hosting-2026" rel="noopener noreferrer"&gt;Best serverless API hosting in 2026&lt;/a&gt;, &lt;a href="https://www.unkey.com/blog/git-push" rel="noopener noreferrer"&gt;git push to deploy&lt;/a&gt;, &lt;a href="https://www.unkey.com/blog/from-endpoint-to-api" rel="noopener noreferrer"&gt;From endpoint to API&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>deployment</category>
      <category>devops</category>
      <category>github</category>
    </item>
    <item>
      <title>How to ratelimit tRPC routes with Unkey</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Fri, 17 May 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/unkey/how-to-ratelimit-trpc-routes-with-unkey-ppi</link>
      <guid>https://dev.to/unkey/how-to-ratelimit-trpc-routes-with-unkey-ppi</guid>
      <description>&lt;p&gt;Ratelimiting is not just a feature; it's a lifeline for production applications. Without it, you could face a skyrocketing bill. Your server could be pushed to its limits, leaving real users stranded and your application's reputation at stake.&lt;/p&gt;

&lt;p&gt;Unkey provides ratelimiting that is distributed globally and can be easily added to any server, ensuring your protection. We will discuss the features of our service, including synchronous and asynchronous protection, identifier overrides, and how our analytical data can help you identify spikes in usage and how it can be used in a tRPC application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To get up and running with the app and follow along, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fundamental understanding of Next.js, primarily regarding routes and server-side data loading.&lt;/li&gt;
&lt;li&gt;Basic familiarity with tRPC.&lt;/li&gt;
&lt;li&gt;An application with user authentication implemented. This example uses Auth.js, but you can use any provider you like.&lt;/li&gt;
&lt;li&gt;Access to your &lt;code&gt;UNKEY_ROOT_KEY&lt;/code&gt;, which you can get by signing up for a free account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we will use create-t3-app for the demo, so feel free to use that if you want an easy way to use tRPC + Auth.js in a Next.js application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the &lt;code&gt;@unkey/ratelimit&lt;/code&gt; package
&lt;/h2&gt;

&lt;p&gt;Before we start coding, we need to install the &lt;code&gt;@unkey/ratelimit&lt;/code&gt; package. This package gives you access to Unkey's API with type safety.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @unkey/ratelimit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating our env
&lt;/h3&gt;

&lt;p&gt;We need to use the &lt;code&gt;UNKEY_ROOT_KEY&lt;/code&gt; to run our ratelimiting package, so we must first update the &lt;code&gt;env.js&lt;/code&gt; file in the &lt;code&gt;src&lt;/code&gt; directory. Add &lt;code&gt;UNKEY_ROOT_KEY: z.string()&lt;/code&gt; to the &lt;code&gt;server&lt;/code&gt; object and &lt;code&gt;UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY&lt;/code&gt; to the &lt;code&gt;runtimeEnv&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;Now that it is updated add your Unkey root key to your .env as &lt;code&gt;UNKEY_ROOT_KEY&lt;/code&gt; which can be found in the Unkey dashboard under settings Root Keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding ratelimiting to a procedure
&lt;/h2&gt;

&lt;p&gt;Now that the package is installed and our &lt;code&gt;.env&lt;/code&gt; has been updated, we can configure our ratelimiter. Inside the &lt;code&gt;server/api/routers/post&lt;/code&gt; file, we have a &lt;code&gt;create&lt;/code&gt; procedure. This procedure allows users to create posts; currently, users can create as many as they like and as quickly as they like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure our ratelimiter
&lt;/h3&gt;

&lt;p&gt;In this example, we will configure our ratelimiter in the procedure itself. Of course, you can abstract this into a utility file if you prefer. First, we must import &lt;code&gt;Ratelimit&lt;/code&gt; from the &lt;code&gt;@unkey/ratelimit&lt;/code&gt; package and &lt;code&gt;TRPCError&lt;/code&gt; and &lt;code&gt;env&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";import { createTRPCRouter, protectedProcedure, publicProcedure,} from "~/server/api/trpc";import { posts } from "~/server/db/schema";import { env } from "~/env";import { TRPCError } from "@trpc/server";import { Ratelimit } from "@unkey/ratelimit";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To configure the Ratelimiter, we need to pass four things along, the root key, the namespace, the limit, and the duration of our ratelimiting. Inside the mutation, add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const unkey = new Ratelimit({ rootKey: env.UNKEY_ROOT_KEY, namespace: "posts.create", limit: 3, duration: "5s",});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The namespace can be anything, but we are using the tRPC route and procedure to make it easier to track in Unkey's analytics. We now have the ability to rate-limit this procedure, allowing only three requests per five seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using our ratelimiting
&lt;/h3&gt;

&lt;p&gt;To use the ratelimit, we need an identifier. This can be anything you like, such as a user ID or an IP address. We will be using our user's ID as they are required to be logged in to create a new post. Then, we can call &lt;code&gt;unkey.limit&lt;/code&gt; with the identifier, and unkey will return a boolean of true or false, which we can use to make a decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { success } = await unkey.limit(ctx.session.user.id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now we have the boolean we can check if it's false and then throw a TRPCError telling the user they have been ratelimited and stop any more logic running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { success } = await unkey.limit(ctx.session.user.id);if (!success) { throw new TRPCError({ code: "TOO_MANY_REQUESTS" });}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, our code is ready to test. Give it a whirl, and try posting multiple times. You will see that the posts won't update anymore after you are rate-limited.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about more expensive requests?
&lt;/h2&gt;

&lt;p&gt;Unkey allows you to tell us how expensive a request should be. For example, maybe you have an AI route that costs you a lot more than any other route, so you want to reduce the number of requests that can be used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { success } = await unkey.limit(ctx.session.user.id, { cost: 3,});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This request costs three instead of one, giving you extra flexibility around expensive routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Faster response
&lt;/h2&gt;

&lt;p&gt;Although Unkey response times are fast, there are some cases where you are willing to give up some accuracy in favor of quicker response times. You can use our &lt;code&gt;async&lt;/code&gt; option, which has 98% accuracy, but we don't need to confirm the limit with the origin before returning a decision. You can set this either on the &lt;code&gt;limit&lt;/code&gt; request or on the configuration itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const unkey = new Ratelimit({ rootKey: env.UNKEY_ROOT_KEY, namespace: "posts.create", limit: 3, duration: "5s", async: true,});// orconst { success } = await unkey.limit(ctx.session.user.id, { async: true,});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this is a small overview of using Unkey's ratelimiting with tRPC, we also offer other features that aren't covered here, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overrides for specific identifiers&lt;/li&gt;
&lt;li&gt;Metadata&lt;/li&gt;
&lt;li&gt;Resources flagging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read more about those features in our documentation on &lt;a href="https://www.unkey.com/docs/ratelimiting/introduction" rel="noopener noreferrer"&gt;Ratelimiting&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>ratelimit</category>
      <category>otp</category>
    </item>
    <item>
      <title>Unkey raises 1.5 million pre-seed</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Wed, 15 Nov 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/unkey/unkey-raises-15-million-pre-seed-5f4n</link>
      <guid>https://dev.to/unkey/unkey-raises-15-million-pre-seed-5f4n</guid>
      <description>&lt;p&gt;The Unkey pre-seed round was led by &lt;a href="https://www.essencevc.fund/" rel="noopener noreferrer"&gt;Essence VC&lt;/a&gt; and joined by &lt;a href="https://www.linkedin.com/in/liujiang1/" rel="noopener noreferrer"&gt;Sunflower Capital&lt;/a&gt;, &lt;a href="https://new-normal.ventures/" rel="noopener noreferrer"&gt;The Normal Fund&lt;/a&gt;, and some fantastic friends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essence VC leads our round.
&lt;/h2&gt;

&lt;p&gt;When we met Tim Chen for the first time, Andreas and I were blown away by his experience and his immediate understanding of our vision. We only needed ten minutes before we were bouncing ideas and thinking about where to go with Unkey.&lt;/p&gt;

&lt;p&gt;Tim also introduced us to our other partners (Sunflower Capital and The New Normal Fund), who were just as excited to see how API authentication and authorization could be simplified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Angels
&lt;/h3&gt;

&lt;p&gt;Our angels are a fantastic group of people in no particular order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Andrew Miklas (Functional Capital / Ex PageDuty Co Founder)&lt;/li&gt;
&lt;li&gt;Ant Wilson &amp;amp; Rory Wilding (Supabase Co Founder / Head of Growth at Supabase)&lt;/li&gt;
&lt;li&gt;Paul Copplestone (Supabase Co Founder)&lt;/li&gt;
&lt;li&gt;Theo Browne (Ping / YouTube / Twitch / Twitter)&lt;/li&gt;
&lt;li&gt;Ian Livingstone (Startup Advisor / Investor)&lt;/li&gt;
&lt;li&gt;Preston-Werner Ventures (Led by Tom Preston-Werner Co-Founder of GitHub)&lt;/li&gt;
&lt;li&gt;George &amp;amp; Chris Perkins (James mum and dad)&lt;/li&gt;
&lt;li&gt;Zain Allarakhia (ex Pipe)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it all started
&lt;/h2&gt;

&lt;p&gt;Andreas and I started Unkey in the middle of June and launched on June 21st, the community loved Unkey and we got some awesome feedback. Once we understood that this side project, which started as a way to solve an annoying problem we both had experienced, was needed by more and more people, we set our sights on building an entire business for it.&lt;/p&gt;

&lt;p&gt;We worked nights and weekends for months to ensure Unkey made scaling user-facing APIs more accessible than ever. Shout out to all the friends and family who supported us getting through the 16+ hour days.&lt;/p&gt;

&lt;h2&gt;
  
  
  How's it going
&lt;/h2&gt;

&lt;p&gt;Since the launch in June, we have had success with the product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.2k stars on &lt;a href="https://github.com/unkeyed/unkey" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;We have done over 13 million API key verifications.&lt;/li&gt;
&lt;li&gt;1.6k users have signed up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Now that Unkey is funded, we can focus on building the best developer experience in the API space. You may be wondering what is next.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hiring 1-2 more engineers.&lt;/li&gt;
&lt;li&gt;Create an easy-to-use permission system that allows you to control access through RBAC, ABAC, or REBAC.&lt;/li&gt;
&lt;li&gt;Create a new gateway system that allows any developer to understand and configure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talking about hiring, if you are interested in being part of the team. Check out the &lt;a href="https://dev.to/careers"&gt;job posting and apply&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for all the support, and we look forward to bringing API scalability to developers like never before.&lt;/p&gt;

</description>
      <category>preseed</category>
      <category>vcs</category>
    </item>
  </channel>
</rss>
