<?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: Kiell Tampubolon</title>
    <description>The latest articles on DEV Community by Kiell Tampubolon (@kielltampubolon).</description>
    <link>https://dev.to/kielltampubolon</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%2Fuser%2Fprofile_image%2F3890870%2Ff4c1760b-670f-4d29-b0a8-29dc39842afa.jpg</url>
      <title>DEV Community: Kiell Tampubolon</title>
      <link>https://dev.to/kielltampubolon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kielltampubolon"/>
    <language>en</language>
    <item>
      <title>From DNS Security Trainer to ICANN87 Fellow: What a Cybersecurity Engineer Looks for in Bali</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:30:21 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/from-dns-security-trainer-to-icann87-fellow-what-a-cybersecurity-engineer-looks-for-in-bali-nbm</link>
      <guid>https://dev.to/kielltampubolon/from-dns-security-trainer-to-icann87-fellow-what-a-cybersecurity-engineer-looks-for-in-bali-nbm</guid>
      <description>&lt;p&gt;In July 2024 I sat in a training room in Nusa Dua with a laptop and too many DNS diagrams. PANDI, the registry that runs Indonesia's .id domain, was running a Training of Trainer program, and some of the instructors came from ICANN. We started at "how does a name turn into an IP address" and ended at DNSSEC signatures and chains of trust. I went home with a notebook full of drawings and a habit I still have: I can't look at a phishing link anymore without wondering who registered that domain, and when.&lt;/p&gt;

&lt;p&gt;Next week I'm going back to the same stretch of coast. ICANN87, ICANN's Annual General Meeting, runs from 17 to 22 October 2026 at the Bali International Convention Centre in Nusa Dua, and I'll be there as one of PANDI's fellows.&lt;/p&gt;

&lt;p&gt;I'm excited about it, the kind of excited where you read meeting schedules for fun. So before I land, I want to write down what I hope to learn and what I want to give back, and check myself against it afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internet's address book has a meeting
&lt;/h2&gt;

&lt;p&gt;Most people in my classes have never heard of ICANN, and that's fine. Every time you open a website, send an email or let an AI agent fetch a URL, something has to turn a name into an address. That something is DNS. ICANN coordinates the top of that system: the root zone, the rules for top-level domains, and the policy work that decides how names are handed out and what happens when they're abused.&lt;/p&gt;

&lt;p&gt;The part that surprised me when I first learned it is who writes those rules. No single company or government does. Governments, registries like PANDI, registrars, security advisors and ordinary internet users each have a seat, and they work things out in open meetings. ICANN87 is one of those meetings, and the Annual General Meeting is the biggest one of the year.&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%2Fraw.githubusercontent.com%2Fglatinone%2Fpersonal-portofolio%2Fmain%2Fpublic%2Fimages%2Ficann87%2F2-dns-who-does-what.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%2Fraw.githubusercontent.com%2Fglatinone%2Fpersonal-portofolio%2Fmain%2Fpublic%2Fimages%2Ficann87%2F2-dns-who-does-what.png" alt="Diagram of the DNS hierarchy from the root zone to .id to domain names to users, next to the ICANN stakeholder groups" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Simplified: who runs which layer of the DNS, and who shapes the rules&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where I'm coming from
&lt;/h2&gt;

&lt;p&gt;I'm not a policy person. My day job is security engineering, and a lot of it happens at the receiving end of DNS problems. When a suspicious email comes in, one of the first things I look at is the sender's domain. How old is it? Does it copy a real brand with one letter swapped? Did it even exist last month? Lookalike domains are a big part of the phishing I triage, so I've spent a lot of time on the abuse side of the DNS without ever seeing how the people who run it think about the problem.&lt;/p&gt;

&lt;p&gt;Outside work, I've been a .id Academy trainer with PANDI since 2024, running bootcamps at UNIMA in Manado and ITEBA in Batam. The rooms are a mix of students, lecturers and people who just bought a .id domain for their small business. And these days a growing part of my work is AI agent and MCP security, which brings DNS back in a way I didn't expect. An agent that browses or calls tools resolves domain names too, and it trusts the answer the same way a browser does, often with no human looking at the link first.&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%2Fraw.githubusercontent.com%2Fglatinone%2Fpersonal-portofolio%2Fmain%2Fpublic%2Fimages%2Ficann87%2F3-two-years-same-island.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%2Fraw.githubusercontent.com%2Fglatinone%2Fpersonal-portofolio%2Fmain%2Fpublic%2Fimages%2Ficann87%2F3-two-years-same-island.png" alt="Timeline from PANDI Training of Trainer in Nusa Dua in July 2024, DNS APAC 2024, .id Academy bootcamps, to ICANN87 in October 2026" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two years from a DNS classroom in Nusa Dua to ICANN87 in Nusa Dua&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What I'm looking for in Bali
&lt;/h2&gt;

&lt;p&gt;First, DNS abuse from the policy side. I know what abuse looks like in an inbox. I know much less about how registries and registrars decide what counts as abuse, how fast they're expected to act, and where it gets hard, like false positives or taking down a domain that turns out to be legitimate. I want to hear that argument from people who deal with it every day.&lt;/p&gt;

&lt;p&gt;Second, DNSSEC in real deployments. In the ToT we learned how DNSSEC signs DNS answers so a resolver can tell if someone tampered with them. ICANN meetings usually include a DNSSEC and security workshop, and I want operators' stories about what breaks in practice. My students ask me that question and right now I only have textbook answers.&lt;/p&gt;

&lt;p&gt;Here's the kind of check I want every student to be able to run on their own domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Is the zone signed? RRSIG records in the answer mean yes.&lt;/span&gt;
dig +dnssec SOA &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Does my resolver validate? Look for the "ad" flag in the header.&lt;/span&gt;
dig +dnssec icann.org A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, AI agents as DNS clients. This one is a hunch more than a plan. Agents are turning into heavy DNS users without anyone deciding that on purpose. I'm curious whether people in the ICANN community are already talking about it, and if they aren't, whether there's a useful paper in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want to give back
&lt;/h2&gt;

&lt;p&gt;PANDI asks each fellow to turn the week into a contribution, written up as a paper. I like that a lot. It gives the week a deliverable, and I work better with one. My rough plan is a loop: listen, connect what I hear to what I see at work, write it up for PANDI, turn it into .id Academy material, and publish the open parts here.&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%2Fraw.githubusercontent.com%2Fglatinone%2Fpersonal-portofolio%2Fmain%2Fpublic%2Fimages%2Ficann87%2F4-contribution-loop.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%2Fraw.githubusercontent.com%2Fglatinone%2Fpersonal-portofolio%2Fmain%2Fpublic%2Fimages%2Ficann87%2F4-contribution-loop.png" alt="Five-step loop: listen at ICANN87, connect to security practice, write a paper for PANDI, teach at .id Academy, share in public" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The plan for the week, and what happens after it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I haven't locked the paper topic yet. I'm leaning toward the place where my two worlds overlap: what DNS abuse looks like from a defender's inbox, and what Indonesian domain owners and trainers can realistically do about it. If the AI agent thread turns out to have legs, that could become its own piece.&lt;/p&gt;

&lt;p&gt;The classroom part matters most to me. When I teach in Manado or Batam, students rarely ask about root servers. They ask how to tell if a link is safe, or what to do when someone makes a fake copy of their campus website. If I can walk back into those rooms with better answers, and with names of the people and processes that handle these problems, the week will have been worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'll be there
&lt;/h2&gt;

&lt;p&gt;If you're at ICANN87, in Nusa Dua or online, and you work on DNS abuse, DNSSEC or anything where AI agents meet the DNS, I'd like to talk. I'll be the one taking too many notes. I'll post what I learn after the meeting, including whatever this post got wrong.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>dns</category>
    </item>
    <item>
      <title>From DNS Security Trainer to ICANN87 Fellow: What a Cybersecurity Engineer Looks for in Bali</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:08:16 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/from-dns-security-trainer-to-icann87-fellow-what-a-cybersecurity-engineer-looks-for-in-bali-52jg</link>
      <guid>https://dev.to/kielltampubolon/from-dns-security-trainer-to-icann87-fellow-what-a-cybersecurity-engineer-looks-for-in-bali-52jg</guid>
      <description>&lt;p&gt;In July 2024 I sat in a training room in Nusa Dua with a laptop and too many DNS diagrams. PANDI, the registry that runs Indonesia's .id domain, was running a Training of Trainer program, and some of the instructors came from ICANN. We started at "how does a name turn into an IP address" and ended at DNSSEC signatures and chains of trust. I went home with a notebook full of drawings and a habit I still have: I can't look at a phishing link anymore without wondering who registered that domain, and when.&lt;/p&gt;

&lt;p&gt;Next week I'm going back to the same stretch of coast. ICANN87, ICANN's Annual General Meeting, runs from 17 to 22 October 2026 at the Bali International Convention Centre in Nusa Dua, and I'll be there as one of PANDI's fellows.&lt;/p&gt;

&lt;p&gt;I'm excited about it, the kind of excited where you read meeting schedules for fun. So before I land, I want to write down what I hope to learn and what I want to give back, and check myself against it afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The internet's address book has a meeting
&lt;/h2&gt;

&lt;p&gt;Most people in my classes have never heard of ICANN, and that's fine. Every time you open a website, send an email or let an AI agent fetch a URL, something has to turn a name into an address. That something is DNS. ICANN coordinates the top of that system: the root zone, the rules for top-level domains, and the policy work that decides how names are handed out and what happens when they're abused.&lt;/p&gt;

&lt;p&gt;The part that surprised me when I first learned it is who writes those rules. No single company or government does. Governments, registries like PANDI, registrars, security advisors and ordinary internet users each have a seat, and they work things out in open meetings. ICANN87 is one of those meetings, and the Annual General Meeting is the biggest one of the year.&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%2Ffqr7k52yq09975kjlv8v.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%2Ffqr7k52yq09975kjlv8v.png" alt="Diagram of the DNS hierarchy from the root zone to .id to domain names to users, next to the ICANN stakeholder groups" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Simplified: who runs which layer of the DNS, and who shapes the rules&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where I'm coming from
&lt;/h2&gt;

&lt;p&gt;I'm not a policy person. My day job is security engineering, and a lot of it happens at the receiving end of DNS problems. When a suspicious email comes in, one of the first things I look at is the sender's domain. How old is it? Does it copy a real brand with one letter swapped? Did it even exist last month? Lookalike domains are a big part of the phishing I triage, so I've spent a lot of time on the abuse side of the DNS without ever seeing how the people who run it think about the problem.&lt;/p&gt;

&lt;p&gt;Outside work, I've been a .id Academy trainer with PANDI since 2024, running bootcamps at UNIMA in Manado and ITEBA in Batam. The rooms are a mix of students, lecturers and people who just bought a .id domain for their small business. And these days a growing part of my work is AI agent and MCP security, which brings DNS back in a way I didn't expect. An agent that browses or calls tools resolves domain names too, and it trusts the answer the same way a browser does, often with no human looking at the link first.&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%2Fu0a8i0juy5sxdqxefv6u.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%2Fu0a8i0juy5sxdqxefv6u.png" alt="Timeline from PANDI Training of Trainer in Nusa Dua in July 2024, DNS APAC 2024, .id Academy bootcamps, to ICANN87 in October 2026" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Two years from a DNS classroom in Nusa Dua to ICANN87 in Nusa Dua&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What I'm looking for in Bali
&lt;/h2&gt;

&lt;p&gt;First, DNS abuse from the policy side. I know what abuse looks like in an inbox. I know much less about how registries and registrars decide what counts as abuse, how fast they're expected to act, and where it gets hard, like false positives or taking down a domain that turns out to be legitimate. I want to hear that argument from people who deal with it every day.&lt;/p&gt;

&lt;p&gt;Second, DNSSEC in real deployments. In the ToT we learned how DNSSEC signs DNS answers so a resolver can tell if someone tampered with them. ICANN meetings usually include a DNSSEC and security workshop, and I want operators' stories about what breaks in practice. My students ask me that question and right now I only have textbook answers.&lt;/p&gt;

&lt;p&gt;Here's the kind of check I want every student to be able to run on their own domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Is the zone signed? RRSIG records in the answer mean yes.&lt;/span&gt;
dig +dnssec SOA &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Does my resolver validate? Look for the "ad" flag in the header.&lt;/span&gt;
dig +dnssec icann.org A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, AI agents as DNS clients. This one is a hunch more than a plan. Agents are turning into heavy DNS users without anyone deciding that on purpose. I'm curious whether people in the ICANN community are already talking about it, and if they aren't, whether there's a useful paper in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want to give back
&lt;/h2&gt;

&lt;p&gt;PANDI asks each fellow to turn the week into a contribution, written up as a paper. I like that a lot. It gives the week a deliverable, and I work better with one. My rough plan is a loop: listen, connect what I hear to what I see at work, write it up for PANDI, turn it into .id Academy material, and publish the open parts here.&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%2Foe33q9xu5klknliprsa5.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%2Foe33q9xu5klknliprsa5.png" alt="Five-step loop: listen at ICANN87, connect to security practice, write a paper for PANDI, teach at .id Academy, share in public" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The plan for the week, and what happens after it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I haven't locked the paper topic yet. I'm leaning toward the place where my two worlds overlap: what DNS abuse looks like from a defender's inbox, and what Indonesian domain owners and trainers can realistically do about it. If the AI agent thread turns out to have legs, that could become its own piece.&lt;/p&gt;

&lt;p&gt;The classroom part matters most to me. When I teach in Manado or Batam, students rarely ask about root servers. They ask how to tell if a link is safe, or what to do when someone makes a fake copy of their campus website. If I can walk back into those rooms with better answers, and with names of the people and processes that handle these problems, the week will have been worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'll be there
&lt;/h2&gt;

&lt;p&gt;If you're at ICANN87, in Nusa Dua or online, and you work on DNS abuse, DNSSEC or anything where AI agents meet the DNS, I'd like to talk. I'll be the one taking too many notes. I'll post what I learn after the meeting, including whatever this post got wrong.&lt;/p&gt;

</description>
      <category>dns</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Cara Bypass Rate Limit API Secara Etis Pakai Local Proxy</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:35:13 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-45d4</link>
      <guid>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-45d4</guid>
      <description>&lt;h2&gt;
  
  
  Cerita di balik masalah
&lt;/h2&gt;

&lt;p&gt;Kamu sedang menyambungkan automation ke sebuah API. Script jalan lancar lima menit pertama, lalu mulai muntah 429. Kamu tambah delay, tetap kena. Kamu bagi proses jadi beberapa worker, kena lagi.&lt;/p&gt;

&lt;p&gt;Rate limit itu bukan musuh. Itu cara penyedia API melindungi infrastrukturnya dan menjaga kualitas layanan untuk semua orang. Masalahnya, banyak client menulis ulang logika penanganan limit yang sama di setiap project: retry di sini, sleep di sana, dan satu worker nakal yang merusak semuanya.&lt;/p&gt;

&lt;p&gt;Solusi yang rapi: taruh satu komponen di tengah, yaitu local proxy. Semua request dari aplikasimu lewat proxy ini, dan proxy yang menangani rate limit secara terpusat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapan kita butuh proxy lokal
&lt;/h2&gt;

&lt;p&gt;Tiga situasi di mana proxy lokal masuk akal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retry backoff. Kamu tidak mau setiap service punya logika retry sendiri. Satu tempat, satu kebijakan.&lt;/li&gt;
&lt;li&gt;Queueing. Saat limit hampir habis, request antre dulu, bukan langsung gagal.&lt;/li&gt;
&lt;li&gt;Multi endpoint pooling. Kalau penyedia API memberi beberapa key atau endpoint, proxy bisa memutar beban antar keduanya. Catatan penting: rotasi key untuk menghindari limit adalah pelanggaran ToS di banyak layanan. Gunakan pooling hanya kalau ToS mengizinkan, misalnya beberapa akun organisasi yang memang sah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Arsitekturnya sederhana
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------+     +-------------+     +--------------+
| Client | --&amp;gt; | Local Proxy | --&amp;gt; | Upstream API |
+--------+     +-------------+     +--------------+
                     |
              +------+------+
              | Token bucket |
              | Queue        |
              | Retry logic  |
              | Cache        |
              +--------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client hanya tahu satu alamat: proxy. Proxy yang tahu kapan harus menahan, kapan harus mencoba ulang, dan kapan boleh langsung lewat.&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%2Fwwljqfzppy9roxjz2hvg.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%2Fwwljqfzppy9roxjz2hvg.png" alt="Diagram arsitektur client ke local proxy ke upstream API" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementasi minimal dengan Python
&lt;/h2&gt;

&lt;p&gt;Ini versi kecil pakai FastAPI dan httpx, sekitar 60 baris:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;

&lt;span class="n"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;          &lt;span class="c1"&gt;# request per detik
&lt;/span&gt;&lt;span class="n"&gt;_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@_app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{path:path}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry-after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# retry sekali lagi
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jalankan dengan &lt;code&gt;uvicorn proxy:app --port 8080&lt;/code&gt;, lalu arahkan semua client ke &lt;code&gt;http://localhost:8080&lt;/code&gt;. Selesai. Logika limit kini hidup di satu tempat.&lt;/p&gt;

&lt;p&gt;Versi Node.js juga ringkas, tapi polanya sama: middleware yang menghitung token, queue yang menahan request, dan retry yang menghormati header Retry-After.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips etis
&lt;/h2&gt;

&lt;p&gt;Bagian paling penting dari artikel ini. Proxy lokal adalah alat untuk jadi client yang lebih baik, bukan untuk lolos dari aturan.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hormati ToS. Baca ketentuan layanan sebelum membangun apa pun. Kalau ToS melarang rotasi akun atau key, jangan lakukan. Kalau melarang scraping, jangan scraping.&lt;/li&gt;
&lt;li&gt;Hormati Retry-After. Kalau server memberi tahu kapan boleh mencoba lagi, patuhi angkanya. Jangan paksa.&lt;/li&gt;
&lt;li&gt;Cache respons. Rate limit paling murah adalah yang tidak perlu dikirim. Cache GET dengan TTL wajar, invalidasi saat ada write.&lt;/li&gt;
&lt;li&gt;Exponential backoff dengan jitter. Jangan retry semua worker di detik yang sama. Tambahkan noise acak supaya tidak terjadi thundering herd.&lt;/li&gt;
&lt;li&gt;Circuit breaker. Kalau upstream terus gagal, berhenti menghantam. Tandai circuit terbuka selama beberapa detik, lempar error yang jujur ke caller, lalu coba lagi pelan-pelan.&lt;/li&gt;
&lt;li&gt;Pantau konsumsi limitmu sendiri. Banyak API mengembalikan sisa kuota di header (X-RateLimit-Remaining dan teman-temannya). Simpan nilai itu di proxy dan perlambat sebelum 429 datang, bukan sesudah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Penutup
&lt;/h2&gt;

&lt;p&gt;Rate limit bukan teka-teki untuk dipecahkan dengan trik. Ia sinyal dari server soal ritme yang wajar. Local proxy membuat codebase-mu mengikuti ritme itu secara otomatis: satu tempat untuk throttle, retry, dan cache, tanpa menyebar logika ke tiap service.&lt;/p&gt;

</description>
      <category>apiwebdevpythonbeginners</category>
    </item>
    <item>
      <title>Cara Bypass Rate Limit API Secara Etis Pakai Local Proxy</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:28:41 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-2j89</link>
      <guid>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-2j89</guid>
      <description>&lt;h2&gt;
  
  
  Cerita di balik masalah
&lt;/h2&gt;

&lt;p&gt;Kamu sedang menyambungkan automation ke sebuah API. Script jalan lancar lima menit pertama, lalu mulai muntah 429. Kamu tambah delay, tetap kena. Kamu bagi proses jadi beberapa worker, kena lagi.&lt;/p&gt;

&lt;p&gt;Rate limit itu bukan musuh. Itu cara penyedia API melindungi infrastrukturnya dan menjaga kualitas layanan untuk semua orang. Masalahnya, banyak client menulis ulang logika penanganan limit yang sama di setiap project: retry di sini, sleep di sana, dan satu worker nakal yang merusak semuanya.&lt;/p&gt;

&lt;p&gt;Solusi yang rapi: taruh satu komponen di tengah, yaitu local proxy. Semua request dari aplikasimu lewat proxy ini, dan proxy yang menangani rate limit secara terpusat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapan kita butuh proxy lokal
&lt;/h2&gt;

&lt;p&gt;Tiga situasi di mana proxy lokal masuk akal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retry backoff. Kamu tidak mau setiap service punya logika retry sendiri. Satu tempat, satu kebijakan.&lt;/li&gt;
&lt;li&gt;Queueing. Saat limit hampir habis, request antre dulu, bukan langsung gagal.&lt;/li&gt;
&lt;li&gt;Multi endpoint pooling. Kalau penyedia API memberi beberapa key atau endpoint, proxy bisa memutar beban antar keduanya. Catatan penting: rotasi key untuk menghindari limit adalah pelanggaran ToS di banyak layanan. Gunakan pooling hanya kalau ToS mengizinkan, misalnya beberapa akun organisasi yang memang sah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Arsitekturnya sederhana
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------+     +-------------+     +--------------+
| Client | --&amp;gt; | Local Proxy | --&amp;gt; | Upstream API |
+--------+     +-------------+     +--------------+
                     |
              +------+------+
              | Token bucket |
              | Queue        |
              | Retry logic  |
              | Cache        |
              +--------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client hanya tahu satu alamat: proxy. Proxy yang tahu kapan harus menahan, kapan harus mencoba ulang, dan kapan boleh langsung lewat.&lt;/p&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt="Diagram arsitektur: client ke local proxy ke upstream API"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementasi minimal dengan Python
&lt;/h2&gt;

&lt;p&gt;Ini versi kecil pakai FastAPI dan httpx, sekitar 60 baris:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;

&lt;span class="n"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;          &lt;span class="c1"&gt;# request per detik
&lt;/span&gt;&lt;span class="n"&gt;_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@_app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{path:path}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry-after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# retry sekali lagi
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jalankan dengan &lt;code&gt;uvicorn proxy:app --port 8080&lt;/code&gt;, lalu arahkan semua client ke &lt;code&gt;http://localhost:8080&lt;/code&gt;. Selesai. Logika limit kini hidup di satu tempat.&lt;/p&gt;

&lt;p&gt;Versi Node.js juga ringkas, tapi polanya sama: middleware yang menghitung token, queue yang menahan request, dan retry yang menghormati header Retry-After.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips etis
&lt;/h2&gt;

&lt;p&gt;Bagian paling penting dari artikel ini. Proxy lokal adalah alat untuk jadi client yang lebih baik, bukan untuk lolos dari aturan.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hormati ToS. Baca ketentuan layanan sebelum membangun apa pun. Kalau ToS melarang rotasi akun atau key, jangan lakukan. Kalau melarang scraping, jangan scraping.&lt;/li&gt;
&lt;li&gt;Hormati Retry-After. Kalau server memberi tahu kapan boleh mencoba lagi, patuhi angkanya. Jangan paksa.&lt;/li&gt;
&lt;li&gt;Cache respons. Rate limit paling murah adalah yang tidak perlu dikirim. Cache GET dengan TTL wajar, invalidasi saat ada write.&lt;/li&gt;
&lt;li&gt;Exponential backoff dengan jitter. Jangan retry semua worker di detik yang sama. Tambahkan noise acak supaya tidak terjadi thundering herd.&lt;/li&gt;
&lt;li&gt;Circuit breaker. Kalau upstream terus gagal, berhenti menghantam. Tandai circuit terbuka selama beberapa detik, lempar error yang jujur ke caller, lalu coba lagi pelan-pelan.&lt;/li&gt;
&lt;li&gt;Pantau konsumsi limitmu sendiri. Banyak API mengembalikan sisa kuota di header (X-RateLimit-Remaining dan teman-temannya). Simpan nilai itu di proxy dan perlambat sebelum 429 datang, bukan sesudah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Penutup
&lt;/h2&gt;

&lt;p&gt;Rate limit bukan teka-teki untuk dipecahkan dengan trik. Ia sinyal dari server soal ritme yang wajar. Local proxy membuat codebase-mu mengikuti ritme itu secara otomatis: satu tempat untuk throttle, retry, dan cache, tanpa menyebar logika ke tiap service.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Cara Bypass Rate Limit API Secara Etis Pakai Local Proxy</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:15:23 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-1mk2</link>
      <guid>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-1mk2</guid>
      <description>&lt;h2&gt;
  
  
  Cerita di balik masalah
&lt;/h2&gt;

&lt;p&gt;Kamu sedang menyambungkan automation ke sebuah API. Script jalan lancar lima menit pertama, lalu mulai muntah 429. Kamu tambah delay, tetap kena. Kamu bagi proses jadi beberapa worker, kena lagi.&lt;/p&gt;

&lt;p&gt;Rate limit itu bukan musuh. Itu cara penyedia API melindungi infrastrukturnya dan menjaga kualitas layanan untuk semua orang. Masalahnya, banyak client menulis ulang logika penanganan limit yang sama di setiap project: retry di sini, sleep di sana, dan satu worker nakal yang merusak semuanya.&lt;/p&gt;

&lt;p&gt;Solusi yang rapi: taruh satu komponen di tengah, yaitu local proxy. Semua request dari aplikasimu lewat proxy ini, dan proxy yang menangani rate limit secara terpusat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapan kita butuh proxy lokal
&lt;/h2&gt;

&lt;p&gt;Tiga situasi di mana proxy lokal masuk akal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retry backoff. Kamu tidak mau setiap service punya logika retry sendiri. Satu tempat, satu kebijakan.&lt;/li&gt;
&lt;li&gt;Queueing. Saat limit hampir habis, request antre dulu, bukan langsung gagal.&lt;/li&gt;
&lt;li&gt;Multi endpoint pooling. Kalau penyedia API memberi beberapa key atau endpoint, proxy bisa memutar beban antar keduanya. Catatan penting: rotasi key untuk menghindari limit adalah pelanggaran ToS di banyak layanan. Gunakan pooling hanya kalau ToS mengizinkan, misalnya beberapa akun organisasi yang memang sah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Arsitekturnya sederhana
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------+     +-------------+     +--------------+
| Client | --&amp;gt; | Local Proxy | --&amp;gt; | Upstream API |
+--------+     +-------------+     +--------------+
                     |
              +------+------+
              | Token bucket |
              | Queue        |
              | Retry logic  |
              | Cache        |
              +--------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client hanya tahu satu alamat: proxy. Proxy yang tahu kapan harus menahan, kapan harus mencoba ulang, dan kapan boleh langsung lewat.&lt;/p&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt="Diagram arsitektur: client ke local proxy ke upstream API"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementasi minimal dengan Python
&lt;/h2&gt;

&lt;p&gt;Ini versi kecil pakai FastAPI dan httpx, sekitar 60 baris:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;

&lt;span class="n"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;          &lt;span class="c1"&gt;# request per detik
&lt;/span&gt;&lt;span class="n"&gt;_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@_app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{path:path}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry-after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# retry sekali lagi
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jalankan dengan &lt;code&gt;uvicorn proxy:app --port 8080&lt;/code&gt;, lalu arahkan semua client ke &lt;code&gt;http://localhost:8080&lt;/code&gt;. Selesai. Logika limit kini hidup di satu tempat.&lt;/p&gt;

&lt;p&gt;Versi Node.js juga ringkas, tapi polanya sama: middleware yang menghitung token, queue yang menahan request, dan retry yang menghormati header Retry-After.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips etis
&lt;/h2&gt;

&lt;p&gt;Bagian paling penting dari artikel ini. Proxy lokal adalah alat untuk jadi client yang lebih baik, bukan untuk lolos dari aturan.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hormati ToS. Baca ketentuan layanan sebelum membangun apa pun. Kalau ToS melarang rotasi akun atau key, jangan lakukan. Kalau melarang scraping, jangan scraping.&lt;/li&gt;
&lt;li&gt;Hormati Retry-After. Kalau server memberi tahu kapan boleh mencoba lagi, patuhi angkanya. Jangan paksa.&lt;/li&gt;
&lt;li&gt;Cache respons. Rate limit paling murah adalah yang tidak perlu dikirim. Cache GET dengan TTL wajar, invalidasi saat ada write.&lt;/li&gt;
&lt;li&gt;Exponential backoff dengan jitter. Jangan retry semua worker di detik yang sama. Tambahkan noise acak supaya tidak terjadi thundering herd.&lt;/li&gt;
&lt;li&gt;Circuit breaker. Kalau upstream terus gagal, berhenti menghantam. Tandai circuit terbuka selama beberapa detik, lempar error yang jujur ke caller, lalu coba lagi pelan-pelan.&lt;/li&gt;
&lt;li&gt;Pantau konsumsi limitmu sendiri. Banyak API mengembalikan sisa kuota di header (X-RateLimit-Remaining dan teman-temannya). Simpan nilai itu di proxy dan perlambat sebelum 429 datang, bukan sesudah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Penutup
&lt;/h2&gt;

&lt;p&gt;Rate limit bukan teka-teki untuk dipecahkan dengan trik. Ia sinyal dari server soal ritme yang wajar. Local proxy membuat codebase-mu mengikuti ritme itu secara otomatis: satu tempat untuk throttle, retry, dan cache, tanpa menyebar logika ke tiap service.&lt;/p&gt;

</description>
      <category>apiwebdevpythonbeginners</category>
    </item>
    <item>
      <title>Cara Bypass Rate Limit API Secara Etis Pakai Local Proxy</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:10:09 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-3lch</link>
      <guid>https://dev.to/kielltampubolon/cara-bypass-rate-limit-api-secara-etis-pakai-local-proxy-3lch</guid>
      <description>&lt;h1&gt;
  
  
  Cara Bypass Rate Limit API Secara Etis Pakai Local Proxy
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Cerita di balik masalah
&lt;/h2&gt;

&lt;p&gt;Kamu sedang menyambungkan automation ke sebuah API. Script jalan lancar lima menit pertama, lalu mulai muntah 429. Kamu tambah delay, tetap kena. Kamu bagi proses jadi beberapa worker, kena lagi.&lt;/p&gt;

&lt;p&gt;Rate limit itu bukan musuh. Itu cara penyedia API melindungi infrastrukturnya dan menjaga kualitas layanan untuk semua orang. Masalahnya, banyak client menulis ulang logika penanganan limit yang sama di setiap project: retry di sini, sleep di sana, dan satu worker nakal yang merusak semuanya.&lt;/p&gt;

&lt;p&gt;Solusi yang rapi: taruh satu komponen di tengah, yaitu local proxy. Semua request dari aplikasimu lewat proxy ini, dan proxy yang menangani rate limit secara terpusat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kapan kita butuh proxy lokal
&lt;/h2&gt;

&lt;p&gt;Tiga situasi di mana proxy lokal masuk akal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retry backoff. Kamu tidak mau setiap service punya logika retry sendiri. Satu tempat, satu kebijakan.&lt;/li&gt;
&lt;li&gt;Queueing. Saat limit hampir habis, request antre dulu, bukan langsung gagal.&lt;/li&gt;
&lt;li&gt;Multi endpoint pooling. Kalau penyedia API memberi beberapa key atau endpoint, proxy bisa memutar beban antar keduanya. Catatan penting: rotasi key untuk menghindari limit adalah pelanggaran ToS di banyak layanan. Gunakan pooling hanya kalau ToS mengizinkan, misalnya beberapa akun organisasi yang memang sah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Arsitekturnya sederhana
&lt;/h2&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%2Fgist.githubusercontent.com%2Fglatinone%2F34d314a7d07a701c204f8c29bb711275%2Fraw%2Farch.svg" 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%2Fgist.githubusercontent.com%2Fglatinone%2F34d314a7d07a701c204f8c29bb711275%2Fraw%2Farch.svg" alt="Diagram arsitektur client ke local proxy ke upstream API" width="860" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Client hanya tahu satu alamat: proxy. Proxy yang tahu kapan harus menahan, kapan harus mencoba ulang, dan kapan boleh langsung lewat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementasi minimal dengan Python
&lt;/h2&gt;

&lt;p&gt;Ini versi kecil pakai FastAPI dan httpx, sekitar 60 baris:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;

&lt;span class="n"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;          &lt;span class="c1"&gt;# request per detik
&lt;/span&gt;&lt;span class="n"&gt;_app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;_bucket_last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_bucket_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@_app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{path:path}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry-after&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# retry sekali lagi
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jalankan dengan &lt;code&gt;uvicorn proxy:app --port 8080&lt;/code&gt;, lalu arahkan semua client ke &lt;code&gt;http://localhost:8080&lt;/code&gt;. Selesai. Logika limit kini hidup di satu tempat.&lt;/p&gt;

&lt;p&gt;Versi Node.js juga ringkas, tapi polanya sama: middleware yang menghitung token, queue yang menahan request, dan retry yang menghormati header Retry-After.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips etis
&lt;/h2&gt;

&lt;p&gt;Bagian paling penting dari artikel ini. Proxy lokal adalah alat untuk jadi client yang lebih baik, bukan untuk lolos dari aturan.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hormati ToS. Baca ketentuan layanan sebelum membangun apa pun. Kalau ToS melarang rotasi akun atau key, jangan lakukan. Kalau melarang scraping, jangan scraping.&lt;/li&gt;
&lt;li&gt;Hormati Retry-After. Kalau server memberi tahu kapan boleh mencoba lagi, patuhi angkanya. Jangan paksa.&lt;/li&gt;
&lt;li&gt;Cache respons. Rate limit paling murah adalah yang tidak perlu dikirim. Cache GET dengan TTL wajar, invalidasi saat ada write.&lt;/li&gt;
&lt;li&gt;Exponential backoff dengan jitter. Jangan retry semua worker di detik yang sama. Tambahkan noise acak supaya tidak terjadi thundering herd.&lt;/li&gt;
&lt;li&gt;Circuit breaker. Kalau upstream terus gagal, berhenti menghantam. Tandai circuit terbuka selama beberapa detik, lempar error yang jujur ke caller, lalu coba lagi pelan-pelan.&lt;/li&gt;
&lt;li&gt;Pantau konsumsi limitmu sendiri. Banyak API mengembalikan sisa kuota di header (X-RateLimit-Remaining dan teman-temannya). Simpan nilai itu di proxy dan perlambat sebelum 429 datang, bukan sesudah.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Penutup
&lt;/h2&gt;

&lt;p&gt;Rate limit bukan teka-teki untuk dipecahkan dengan trik. Ia sinyal dari server soal ritme yang wajar. Local proxy membuat codebase-mu mengikuti ritme itu secara otomatis: satu tempat untuk throttle, retry, dan cache, tanpa menyebar logika ke tiap service.&lt;/p&gt;

&lt;p&gt;Bangun dia untuk menjadi tetangga yang baik di internet. API-mu, dan tim SRE penyedia layanannya, akan berterima kasih.&lt;/p&gt;

</description>
      <category>api</category>
      <category>proxy</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Best Open-Source Structured Data Extraction Models for Financial Reports</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Tue, 13 Oct 2026 02:30:34 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/best-open-source-structured-data-extraction-models-for-financial-reports-1ji9</link>
      <guid>https://dev.to/kielltampubolon/best-open-source-structured-data-extraction-models-for-financial-reports-1ji9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick answer:&lt;/strong&gt; Among open-weight models with a commercial licence and a size a single team can serve, Qwen3.8-27B is the strongest general extractor, and NuExtract3 is the best purpose-built one. Qwen3.5-4B and Qwen3-4B-Instruct-2507 cover stable, high-volume schemas cheaply, and GLiNER2 and NuNER_Zero tag entities on CPU. SIE serves the document models you put into production, on Cloud or in your cluster, behind one API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The segment table came back clean. Four operating segments, each with a revenue figure, a period and a unit, and the schema validator passed every row. Then someone added the segments up and got a number larger than consolidated revenue. The model had read the "Intersegment eliminations" row as a positive figure, so the eliminations were added back instead of taken away. JSON validators are the most relaxed reviewers in finance. They check that a number is a number and ask nothing about its sign, its scale or which row it came from.&lt;/p&gt;

&lt;p&gt;This ranking covers open-weight models with a licence that permits commercial use, turning report text or page images into fields such as revenue, period, unit, scale and segment. It assumes the text is already available: for EDGAR filings that means the HTML of the filing itself, and OCR only for PDF-only sources, which the companion list of open OCR models covers. Hosted frontier models are not ranked, but they get a direct comparison after the list.&lt;/p&gt;

&lt;p&gt;One point before the models. For a US filer's face statements, you may not need a model at all. The SEC's XBRL APIs return tagged facts in base units. What those APIs leave out is where models earn their place: untagged tables in MD&amp;amp;A and EX-99.1 earnings releases, non-US annual reports that exist only as PDF, and the segment detail discussed below.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Qwen3.8-27B
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model card:&lt;/strong&gt; &lt;a href="https://huggingface.co/Qwen/Qwen3.8-27B" rel="noopener noreferrer"&gt;Qwen/Qwen3.8-27B on Hugging Face&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Qwen3.8-27B is a 27.8B dense model from Qwen with native vision-language input, licensed Apache-2.0, so commercial use is permitted. It reads page images directly, so a PDF page can go in without a separate OCR step when the layout allows.&lt;/p&gt;

&lt;p&gt;It ranks first on evidence and capability together. Its card reports 91.1 on OmniDocBench v1.5, the strongest document-parsing figure among the generative models here. It takes text and images in one request, it can produce structured output under a JSON-schema grammar when the serving layer supports it, and it has a native context of 262,144 tokens, extensible to one million, which fits a full 10-K section with room for the schema. Thinking mode is on by default. For extraction, switch it off per request, or every field pays for reasoning you did not ask for.&lt;/p&gt;

&lt;p&gt;The limit is size. Self-hosting a 27B model needs a large GPU, and the card reports no finance-specific extraction score.&lt;/p&gt;

&lt;p&gt;Deployment shape: open weights. It is priced on SIE Cloud as &lt;code&gt;Qwen/Qwen3.8-27B-FP8&lt;/code&gt;; the self-hosted catalog carries the previous generation, Qwen3.6-27B, also Apache-2.0 at 27.8B. It suits schemas that need judgement: segment tables, footnoted adjustments, and fields that depend on reading two rows together.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. NuExtract3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model card:&lt;/strong&gt; &lt;a href="https://huggingface.co/numind/NuExtract3" rel="noopener noreferrer"&gt;numind/NuExtract3 on Hugging Face&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NuExtract3 from NuMind is built for one job: filling a JSON template from a document. It is a 4B-parameter fine-tune of Qwen3.5-4B, released under Apache-2.0, so commercial use is permitted. It reads text and images, handles multilingual documents, and runs with a 131,072-token context in the vLLM configuration its card gives. NuMind also publishes quantised builds, including FP8 and GGUF.&lt;/p&gt;

&lt;p&gt;The template language is its strength. You describe the fields, and the model returns null or an empty array when a value is absent. In finance that matters: a model that abstains on a missing segment figure is easier to trust than one that invents a plausible number. It also offers reasoning and non-reasoning modes, and it can convert pages to Markdown with tables as HTML.&lt;/p&gt;

&lt;p&gt;The numbers are NuMind's. On its internal benchmark of about 600 documents, described as including invoices, movie posters and floor plans, the card reports 0.651 for NuExtract3 against 0.417 for its base, Qwen3.5-4B. On ExtractBench, the card reports a mean of 82.35. Neither set is described as covering financial filings.&lt;/p&gt;

&lt;p&gt;Deployment shape: open weights you serve yourself, for example with vLLM, as the card shows. It suits teams that want template extraction with abstention from a model sized for one GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Qwen3.5-4B
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model card:&lt;/strong&gt; &lt;a href="https://huggingface.co/Qwen/Qwen3.5-4B" rel="noopener noreferrer"&gt;Qwen/Qwen3.5-4B on Hugging Face&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Qwen3.5-4B is a small generative model, 4.66B parameters in its weights, licensed Apache-2.0, so commercial use is permitted. It shares the family's 262,144-token context and, like Qwen3.8, thinks by default unless you disable it.&lt;/p&gt;

&lt;p&gt;Its place here is cost. For a dozen fields pulled from the same report section each period, a 4B model on a small GPU is often enough, and at volume the saving is the reason to test it. The card also reports an OmniDocBench v1.5 result; the rank here rests on size and cost. Whether it is accurate enough is a question only your evaluation set can answer, so test it on the hardest filers you cover, not the tidiest.&lt;/p&gt;

&lt;p&gt;Deployment shape: open weights, in SIE's self-hosted catalog, and runnable locally. On SIE Cloud, priced extraction runs on Qwen3.8-27B (FP8). The 4B runs self-hosted or locally. It suits high-volume, stable schemas.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Qwen3-4B-Instruct-2507
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model card:&lt;/strong&gt; &lt;a href="https://huggingface.co/Qwen/Qwen3-4B-Instruct-2507" rel="noopener noreferrer"&gt;Qwen/Qwen3-4B-Instruct-2507 on Hugging Face&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Qwen3-4B-Instruct-2507 is an earlier 4B generation, licensed Apache-2.0, so commercial use is permitted. Its card is plain about the difference that matters: it supports only non-thinking mode. Context is 262,144 tokens natively.&lt;/p&gt;

&lt;p&gt;That makes it the predictable choice. With no thinking tokens, latency per field is steady, which suits an extraction service with a response-time budget, and output length is easier to cap. It is older than Qwen3.5-4B, so expect the newer model to edge it on harder pages. Running both on the same evaluation set is cheap and tells you whether the gap matters for your schema.&lt;/p&gt;

&lt;p&gt;Deployment shape: open weights, in SIE's self-hosted catalog, and runnable locally. It suits teams that value consistent latency over the last point of accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. GLiNER2 Large
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model card:&lt;/strong&gt; &lt;a href="https://huggingface.co/fastino/gliner2-large-v1" rel="noopener noreferrer"&gt;fastino/gliner2-large-v1 on Hugging Face&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GLiNER2 from Fastino is an encoder model, 486M parameters in its large weights, licensed Apache-2.0, so commercial use is permitted. Its card describes entity recognition, classification, structured extraction and relation extraction in a single forward pass, and calls it CPU first.&lt;/p&gt;

&lt;p&gt;Speed is the draw. For fields that are named things rather than computed ones, such as company name, auditor, segment label or fiscal year-end, a schema-driven encoder is fast and cheap. It does not generate text and does not reason across tables, so a subtotal that must be checked against two rows is beyond it.&lt;/p&gt;

&lt;p&gt;Deployment shape: open weights that run on CPU; in SIE's self-hosted catalog; and priced on SIE Cloud at $0.186 per million input tokens, as listed on 2026-09-30. It suits the first pass of a pipeline, tagging entities and routing pages before a generative model handles the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. NuNER_Zero
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model card:&lt;/strong&gt; &lt;a href="https://huggingface.co/numind/NuNER_Zero" rel="noopener noreferrer"&gt;numind/NuNER_Zero on Hugging Face&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NuNER_Zero from NuMind is a 449M zero-shot entity tagger, licensed MIT, so commercial use is permitted. You pass the labels as input, and it finds the spans.&lt;/p&gt;

&lt;p&gt;It ranks last because its job is the smallest. It tags spans and nothing more: no schema, and no sense of what a number means. For the simplest fields, such as the company, the auditor or a date, that is enough, and a model this size is cheap to run on every document. Its card has not been updated since February 2025.&lt;/p&gt;

&lt;p&gt;Deployment shape: open weights; in SIE's catalog for self-hosting or local use, and on SIE Cloud. It suits a light entity pass before heavier extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  How these compare with hosted APIs
&lt;/h2&gt;

&lt;p&gt;Hosted frontier models are a separate serving path from the open weights above. &lt;strong&gt;OpenAI&lt;/strong&gt; Structured Outputs guarantees responses that adhere to a supplied JSON schema, and Gemini and Claude offer schema-constrained output too. Prompt caching and batch pricing, about half the list price, cut the cost of stable prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI&lt;/strong&gt; gpt-6.1-sol costs $2 per million input tokens and $10 per million output, and gpt-6-astra $10 and $50. OpenAI also ships open-weight gpt-oss models under Apache-2.0, but the frontier GPT family is hosted only. &lt;strong&gt;Google Gemini&lt;/strong&gt; reads PDFs natively, pages as images, including tables and charts, in documents up to 1,000 pages. Gemini 3.8 Flash is $0.75 and $3.75 at a promotional price through 2026-12-31, and Gemini 3.1 Pro Preview is $2 and $12. &lt;strong&gt;Anthropic Claude&lt;/strong&gt; Sonnet 5.5 costs $2 and $10, with a one-million-token window at standard pricing.&lt;/p&gt;

&lt;p&gt;The cost is structural. Documents go to the provider's service. Regional data residency and zero-retention terms exist, and Azure OpenAI, Vertex AI and Bedrock run inside your cloud tenancy, but the model does not run in your own cluster. Per-token cost also scales with volume. Open models run on SIE, on Cloud or in your own cluster, with weights you pin.&lt;/p&gt;

&lt;p&gt;Schema adherence, from any provider, still only guarantees the shape. The eliminations row in the opening example would have passed strict JSON-schema output, because the error was a sign, not a type.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run these models
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd8fwn5monakf2mhvyfz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd8fwn5monakf2mhvyfz.webp" alt="Superlinked Inference Engine" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://superlinked.com/" rel="noopener noreferrer"&gt;Superlinked&lt;/a&gt; SIE is an inference layer for task-specific models. It serves the document models you put into production, next to the OCR models and embedders a reporting pipeline also needs. It runs in three modes: managed on &lt;a href="https://superlinked.com/cloud" rel="noopener noreferrer"&gt;SIE Cloud&lt;/a&gt;, priced per use with no GPUs to run; self-hosted in your own VPC or Kubernetes cluster, the same engine under Apache 2.0; and locally for development.&lt;/p&gt;

&lt;p&gt;On Cloud, as priced on 2026-09-30, Qwen3.8-27B (FP8) backs doc field extraction and structured output at $0.72 per million tokens in and $0.72 out, GLiNER2 Large is $0.186 per million input tokens, and NuNER_Zero is available. Qwen3.6-27B, Qwen3.5-4B and Qwen3-4B-Instruct-2507 sit in &lt;a href="https://superlinked.com/models" rel="noopener noreferrer"&gt;the model catalog&lt;/a&gt; for self-hosted and local use. The SDK's &lt;code&gt;extract&lt;/code&gt; call takes a model ID and a list of labels, and &lt;a href="https://superlinked.com/docs" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; covers &lt;code&gt;generate&lt;/code&gt; for the Qwen models, so comparing two candidates on one evaluation set needs no second serving stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest take:&lt;/strong&gt; SIE serves the document models you put into production, on SIE Cloud or in your own cluster, behind one API. OCR, extraction, embeddings and reranking share that client. On Cloud, Qwen3.8-27B (FP8) backs field extraction and structured output, with GLiNER2 and NuNER_Zero beside it, and Superlinked runs the GPUs. Self-host the same Apache 2.0 engine on your own GPUs, and the open weights in the catalog, including the Qwen extractors, run under the same model IDs. You point one SDK at a gold set, swap the model ID, and keep the schema, the labels and the serving path stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we ranked them
&lt;/h2&gt;

&lt;p&gt;No benchmark decides this order, because none of the ranked models publishes a financial-report extraction score. The order rests on four criteria: the range of fields a model can handle, from entity labels to computed figures; fit for schema-bound output, including abstention; the publisher's evidence, such as OmniDocBench v1.5 or template-extraction scores, read as attributed claims; and size and cost to run. Licence was a gate, not a score: every entry permits commercial use. Where a model runs is reported in each entry's deployment line and played no part in the order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do we get segment revenue when companyfacts does not have it?
&lt;/h3&gt;

&lt;p&gt;companyfacts, companyconcept and frames include only facts that use a non-custom taxonomy and apply to the entire filing entity, so segment and other dimensional facts are excluded, as are company extension tags. Read them from the filing's own inline XBRL instead: each &lt;code&gt;ix:nonFraction&lt;/code&gt; points to a &lt;code&gt;contextRef&lt;/code&gt;, and that context carries the axis and member, such as &lt;code&gt;StatementBusinessSegmentsAxis&lt;/code&gt; and the segment's member. Extract by axis and member, keep the eliminations member separate with its sign, and check that segments plus eliminations equal the consolidated fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do we stop thousands-versus-millions scale errors?
&lt;/h3&gt;

&lt;p&gt;Make scale a field. Extract the unit and the scale statement ("in thousands", "in millions") as values next to every figure, then normalise in code, never in the prompt. In inline XBRL the scale is markup: a figure displayed as "78,678" carries &lt;code&gt;scale="6"&lt;/code&gt; on its &lt;code&gt;ix:nonFraction&lt;/code&gt; element, so it means 78,678 million, and &lt;code&gt;decimals&lt;/code&gt; (for example -6) states the rounding precision, which is the right tolerance when you match. The JSON APIs return base units with no scale attribute, so compare against those.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should we fine-tune or prompt?
&lt;/h3&gt;

&lt;p&gt;Prompt first. With a typed schema, a few examples and thinking switched off, the models above often clear the bar on stable sections, and you learn where they fail. Fine-tune only when the evaluation shows a consistent error you cannot prompt away, such as a filer layout the model keeps misreading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Start with the schema, not the model. For fields that need judgement, test Qwen3.8-27B; for strict templates with abstention, NuExtract3; for stable high-volume sections, Qwen3.5-4B; for entity fields, GLiNER2. SIE serves the document models you put into production, on Cloud or in your cluster, behind one API. The &lt;a href="https://superlinked.com/cloud" rel="noopener noreferrer"&gt;SIE Cloud page&lt;/a&gt; lists the priced models and a first request you can point at your own reports.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>machinelearning</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Membangun Local API Proxy dengan Token Bucket untuk Rate Limit v2</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:56:11 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/membangun-local-api-proxy-dengan-token-bucket-untuk-rate-limit-v2-3e2a</link>
      <guid>https://dev.to/kielltampubolon/membangun-local-api-proxy-dengan-token-bucket-untuk-rate-limit-v2-3e2a</guid>
      <description>&lt;h1&gt;
  
  
  Membangun Local API Proxy dengan Token Bucket untuk Rate Limit v2
&lt;/h1&gt;

&lt;p&gt;Banyak layanan API menerapkan &lt;strong&gt;rate limit&lt;/strong&gt; untuk melindungi server. Ketika aplikasi melebihi batas, respons &lt;code&gt;429 Too Many Requests&lt;/code&gt; muncul. Solusi klasik &lt;em&gt;retry dengan exponential backoff&lt;/em&gt; tetap membiarkan &lt;em&gt;burst&lt;/em&gt; request ke upstream. Pendekatan lebih baik: &lt;strong&gt;local proxy&lt;/strong&gt; yang menampung request, mengatur keluar ritmis, dan menyimpan cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arsitektur Proxy Lokal
&lt;/h2&gt;

&lt;p&gt;&lt;a href="" class="article-body-image-wrapper"&gt;&lt;img alt="Arsitektur Local Proxy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Penjelasan Alur
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; kirim request ke &lt;strong&gt;Local Proxy&lt;/strong&gt; (bukan langsung upstream).&lt;/li&gt;
&lt;li&gt;Proxy cek &lt;strong&gt;Token Bucket&lt;/strong&gt;: token ada ? lepas ke upstream; tidak ada ? masuk &lt;strong&gt;Request Queue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry Manager&lt;/strong&gt; tangani &lt;code&gt;429&lt;/code&gt; dengan header &lt;code&gt;Retry-After&lt;/code&gt; + jitter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Store&lt;/strong&gt; simpan respons GET sukses; request identik berikutnya ambil dari cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementasi Minimal (FastAPI + httpx)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proxy.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;BURST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="n"&gt;CACHE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BURST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;upstream_req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upstream_req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{path:path}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;
    &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Menjalankan Proxy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fastapi httpx uvicorn
uvicorn proxy:app &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arahkan client ke &lt;code&gt;http://localhost:8000/...&lt;/code&gt; sebagai pengganti upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist Produksi
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Tambahkan &lt;strong&gt;metrics&lt;/strong&gt; (Prometheus) untuk bucket &amp;amp; queue&lt;/li&gt;
&lt;li&gt;[ ] Implementasikan &lt;strong&gt;circuit breaker&lt;/strong&gt; jika upstream gagal&lt;/li&gt;
&lt;li&gt;[ ] Gunakan &lt;strong&gt;Redis&lt;/strong&gt; untuk cache &amp;amp; bucket terdistribusi&lt;/li&gt;
&lt;li&gt;[ ] Tambahkan &lt;strong&gt;authentication&lt;/strong&gt; (API key) pada proxy&lt;/li&gt;
&lt;li&gt;[ ] Tulis &lt;strong&gt;unit &amp;amp; integration test&lt;/strong&gt; untuk retry logic&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kesimpulan
&lt;/h2&gt;

&lt;p&gt;Dengan &lt;strong&gt;local proxy token-bucket&lt;/strong&gt;, aplikasi tidak khawatir &lt;code&gt;429&lt;/code&gt;. Proxy menyerap &lt;em&gt;burst&lt;/em&gt;, atur &lt;em&gt;throughput&lt;/em&gt;, kurangi beban upstream lewat caching. Cocok untuk microservices, scraper, integrasi pihak ketiga.&lt;/p&gt;

</description>
      <category>api</category>
      <category>ratelimit</category>
      <category>proxy</category>
      <category>python</category>
    </item>
    <item>
      <title>Test Article</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:48:59 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/test-article-34e7</link>
      <guid>https://dev.to/kielltampubolon/test-article-34e7</guid>
      <description>&lt;h1&gt;
  
  
  Test
&lt;/h1&gt;

&lt;p&gt;Test content&lt;/p&gt;

</description>
      <category>test</category>
    </item>
    <item>
      <title>Membangun Local API Proxy dengan Token Bucket untuk Menangani Rate Limit</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:34:23 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/membangun-local-api-proxy-dengan-token-bucket-untuk-menangani-rate-limit-28i9</link>
      <guid>https://dev.to/kielltampubolon/membangun-local-api-proxy-dengan-token-bucket-untuk-menangani-rate-limit-28i9</guid>
      <description>&lt;h1&gt;
  
  
  Membangun Local API Proxy dengan Token Bucket untuk Menangani Rate Limit
&lt;/h1&gt;

&lt;p&gt;Banyak layanan API menerapkan &lt;strong&gt;rate limit&lt;/strong&gt; untuk melindungi server dari beban berlebih. Ketika aplikasi melebihi batas, respons &lt;code&gt;429 Too Many Requests&lt;/code&gt; muncul dan proses terhenti.&lt;/p&gt;

&lt;p&gt;Solusi klasik adalah &lt;em&gt;retry dengan exponential backoff&lt;/em&gt;, namun pendekatan itu tetap membiarkan &lt;em&gt;burst&lt;/em&gt; request sampai ke upstream. Pendekatan yang lebih baik: &lt;strong&gt;local proxy&lt;/strong&gt; yang menampung request, mengatur keluar secara ritmis, dan menyimpan cache respons.&lt;/p&gt;

&lt;p&gt;Artikel ini menunjukkan arsitektur dan implementasi minimal menggunakan &lt;strong&gt;Python (FastAPI + httpx)&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arsitektur Proxy Lokal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;svg width="500" height="250" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 250"&amp;gt;
  &amp;lt;!-- Client --&amp;gt;
  &amp;lt;rect x="20" y="80" width="100" height="50" rx="5" fill="#2563eb"/&amp;gt;
  &amp;lt;text x="70" y="110" text-anchor="middle" fill="white" font-size="14" font-family="system-ui, sans-serif"&amp;gt;Client&amp;lt;/text&amp;gt;
  &amp;lt;!-- Arrow 1 --&amp;gt;
  &amp;lt;path d="M120 105 L180 105" stroke="#475569" stroke-width="2" marker-end="url(#arrow)"/&amp;gt;
  &amp;lt;!-- Local Proxy --&amp;gt;
  &amp;lt;rect x="180" y="40" width="140" height="170" rx="8" fill="#fef3c7" stroke="#f59e0b" stroke-width="2"/&amp;gt;
  &amp;lt;text x="250" y="65" text-anchor="middle" fill="#92400e" font-size="14" font-weight="600" font-family="system-ui, sans-serif"&amp;gt;Local Proxy&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="85" text-anchor="middle" fill="#92400e" font-size="11" font-family="system-ui, sans-serif"&amp;gt;Token Bucket&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="105" text-anchor="middle" fill="#92400e" font-size="11" font-family="system-ui, sans-serif"&amp;gt;Request Queue&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="125" text-anchor="middle" fill="#92400e" font-size="11" font-family="system-ui, sans-serif"&amp;gt;Retry Manager&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="145" text-anchor="middle" fill="#92400e" font-size="11" font-family="system-ui, sans-serif"&amp;gt;Cache Store&amp;lt;/text&amp;gt;
  &amp;lt;!-- Arrow 2 --&amp;gt;
  &amp;lt;path d="M320 105 L380 105" stroke="#475569" stroke-width="2" marker-end="url(#arrow)"/&amp;gt;
  &amp;lt;!-- Upstream API --&amp;gt;
  &amp;lt;rect x="380" y="80" width="100" height="50" rx="5" fill="#16a34a"/&amp;gt;
  &amp;lt;text x="430" y="110" text-anchor="middle" fill="white" font-size="14" font-family="system-ui, sans-serif"&amp;gt;Upstream API&amp;lt;/text&amp;gt;
  &amp;lt;!-- Arrow definition --&amp;gt;
  &amp;lt;defs&amp;gt;
    &amp;lt;marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"&amp;gt;
      &amp;lt;path d="M0,0 L0,6 L9,3 z" fill="#475569"/&amp;gt;
    &amp;lt;/marker&amp;gt;
  &amp;lt;/defs&amp;gt;
&amp;lt;/svg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Penjelasan Alur
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; mengirim request ke &lt;strong&gt;Local Proxy&lt;/strong&gt; (bukan langsung ke upstream).&lt;/li&gt;
&lt;li&gt;Proxy memeriksa &lt;strong&gt;Token Bucket&lt;/strong&gt;: apakah token tersedia? Jika ya, request dilepaskan ke upstream; jika tidak, request masuk &lt;strong&gt;Request Queue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry Manager&lt;/strong&gt; menangani respons &lt;code&gt;429&lt;/code&gt; dari upstream dengan membaca header &lt;code&gt;Retry-After&lt;/code&gt; dan menambahkan &lt;em&gt;jitter&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Store&lt;/strong&gt; menyimpan respons GET yang berhasil; request identik berikutnya disajikan dari cache tanpa menghitung ke bucket.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementasi Minimal (FastAPI + httpx)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proxy.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;          &lt;span class="c1"&gt;# request per second
&lt;/span&gt;&lt;span class="n"&gt;BURST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;         &lt;span class="c1"&gt;# bucket capacity
&lt;/span&gt;&lt;span class="n"&gt;CACHE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;    &lt;span class="c1"&gt;# seconds
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


&lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BURST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Cache hit untuk GET
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;upstream_req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upstream_req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{path:path}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;
    &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Menjalankan Proxy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fastapi httpx uvicorn
uvicorn proxy:app &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sekarang arahkan client ke &lt;code&gt;http://localhost:8000/...&lt;/code&gt; sebagai pengganti upstream asli.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist Produksi
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Tambahkan &lt;strong&gt;metrics&lt;/strong&gt; (Prometheus) untuk mengamati bucket &amp;amp; queue&lt;/li&gt;
&lt;li&gt;[ ] Implementasikan &lt;strong&gt;circuit breaker&lt;/strong&gt; jika upstream terus gagal&lt;/li&gt;
&lt;li&gt;[ ] Gunakan &lt;strong&gt;Redis&lt;/strong&gt; untuk cache &amp;amp; bucket terdistribusi&lt;/li&gt;
&lt;li&gt;[ ] Tambahkan &lt;strong&gt;authentication&lt;/strong&gt; (API key) pada proxy&lt;/li&gt;
&lt;li&gt;[ ] Tulis &lt;strong&gt;unit &amp;amp; integration test&lt;/strong&gt; untuk retry logic&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kesimpulan
&lt;/h2&gt;

&lt;p&gt;Dengan &lt;strong&gt;local proxy token-bucket&lt;/strong&gt;, aplikasi tidak lagi perlu khawatir soal &lt;code&gt;429&lt;/code&gt;. Proxy menyerap &lt;em&gt;burst&lt;/em&gt;, mengatur &lt;em&gt;throughput&lt;/em&gt;, dan mengurangi beban upstream lewat caching. Pola ini cocok untuk microservices, scraper, dan integrasi pihak ketiga.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Artikel ini dipublikasikan otomatis melalui pipeline Writer → Designer → Reviewer → Publisher.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>ratelimit</category>
      <category>proxy</category>
      <category>python</category>
    </item>
    <item>
      <title>Panduan API Rate‑Limit: Membangun Local Proxy dengan Token Bucket</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:27:27 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/panduan-api-rate-limit-membangun-local-proxy-dengan-token-bucket-53c4</link>
      <guid>https://dev.to/kielltampubolon/panduan-api-rate-limit-membangun-local-proxy-dengan-token-bucket-53c4</guid>
      <description>&lt;h1&gt;
  
  
  Panduan API Rate‑Limit: Membangun Local Proxy dengan Token Bucket
&lt;/h1&gt;

&lt;p&gt;Banyak layanan API menerapkan &lt;strong&gt;rate limit&lt;/strong&gt; untuk melindungi server dari overload. Ketika aplikasi Anda melebihi batas, respons &lt;code&gt;429 Too Many Requests&lt;/code&gt; muncul dan proses terhenti. Solusi klasik adalah &lt;em&gt;retry dengan exponential backoff&lt;/em&gt;, tetapi pendekatan itu masih membiarkan &lt;em&gt;burst&lt;/em&gt; request sampai ke upstream.&lt;/p&gt;

&lt;p&gt;Pendekatan yang lebih baik: &lt;strong&gt;local proxy&lt;/strong&gt; yang menampung request, mengatur keluar secara ritmis, dan menyimpan cache respons. Artikel ini menunjukkan arsitektur dan implementasi minimal menggunakan Python (FastAPI + httpx).&lt;/p&gt;

&lt;h2&gt;
  
  
  Arsitektur Proxy Lokal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;svg width="500" height="250" xmlns="http://www.w3.org/2000/svg"&amp;gt;
  &amp;lt;rect x="20" y="80" width="100" height="50" rx="5" fill="#4a90d9"/&amp;gt;
  &amp;lt;text x="70" y="110" text-anchor="middle" fill="white" font-size="14"&amp;gt;Client&amp;lt;/text&amp;gt;
  &amp;lt;path d="M120 105 L180 105" stroke="#666" stroke-width="2" marker-end="url(#arrow)"/&amp;gt;
  &amp;lt;rect x="180" y="50" width="140" height="110" rx="5" fill="#f5a623"/&amp;gt;
  &amp;lt;text x="250" y="75" text-anchor="middle" fill="white" font-size="14"&amp;gt;Local Proxy&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="95" text-anchor="middle" fill="white" font-size="11"&amp;gt;Token Bucket&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="115" text-anchor="middle" fill="white" font-size="11"&amp;gt;Request Queue&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="135" text-anchor="middle" fill="white" font-size="11"&amp;gt;Retry Manager&amp;lt;/text&amp;gt;
  &amp;lt;text x="250" y="155" text-anchor="middle" fill="white" font-size="11"&amp;gt;Cache Store&amp;lt;/text&amp;gt;
  &amp;lt;path d="M320 105 L380 105" stroke="#666" stroke-width="2" marker-end="url(#arrow)"/&amp;gt;
  &amp;lt;rect x="380" y="80" width="100" height="50" rx="5" fill="#7ed321"/&amp;gt;
  &amp;lt;text x="430" y="110" text-anchor="middle" fill="white" font-size="14"&amp;gt;Upstream API&amp;lt;/text&amp;gt;
  &amp;lt;defs&amp;gt;
    &amp;lt;marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto" markerUnits="strokeWidth"&amp;gt;
      &amp;lt;path d="M0,0 L0,6 L9,3 z" fill="#666"/&amp;gt;
    &amp;lt;/marker&amp;gt;
  &amp;lt;/defs&amp;gt;
&amp;lt;/svg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Penjelasan alur:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; mengirim request ke &lt;strong&gt;Local Proxy&lt;/strong&gt; (bukan langsung ke upstream).&lt;/li&gt;
&lt;li&gt;Proxy memeriksa &lt;strong&gt;Token Bucket&lt;/strong&gt;: apakah token tersedia? Jika ya, request dilepaskan ke upstream; jika tidak, request masuk &lt;strong&gt;Request Queue&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry Manager&lt;/strong&gt; menangani respons &lt;code&gt;429&lt;/code&gt; dari upstream dengan membaca header &lt;code&gt;Retry-After&lt;/code&gt; dan menambahkan &lt;em&gt;jitter&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Store&lt;/strong&gt; menyimpan respons GET yang berhasil; request identik berikutnya disajikan dari cache tanpa menghitung ke bucket.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementasi Minimal (FastAPI + httpx)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# proxy.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;UPSTREAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;          &lt;span class="c1"&gt;# request per second
&lt;/span&gt;&lt;span class="n"&gt;BURST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;         &lt;span class="c1"&gt;# bucket capacity
&lt;/span&gt;&lt;span class="n"&gt;CACHE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;    &lt;span class="c1"&gt;# seconds
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;burst&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;burst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BURST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPSTREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;upstream_req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upstream_req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/{path:path}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;
    &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Menjalankan Proxy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fastapi httpx uvicorn
uvicorn proxy:app &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sekarang arahkan client ke &lt;code&gt;http://localhost:8000/...&lt;/code&gt; sebagai pengganti upstream asli.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist Produksi
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Tambahkan &lt;strong&gt;metrics&lt;/strong&gt; (Prometheus) untuk mengamati bucket &amp;amp; queue.&lt;/li&gt;
&lt;li&gt;[ ] Implementasikan &lt;strong&gt;circuit breaker&lt;/strong&gt; jika upstream terus gagal.&lt;/li&gt;
&lt;li&gt;[ ] Gunakan &lt;strong&gt;Redis&lt;/strong&gt; untuk cache &amp;amp; bucket terdistribusi.&lt;/li&gt;
&lt;li&gt;[ ] Tambahkan &lt;strong&gt;authentication&lt;/strong&gt; (API key) pada proxy.&lt;/li&gt;
&lt;li&gt;[ ] Tulis &lt;strong&gt;unit &amp;amp; integration test&lt;/strong&gt; untuk retry logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kesimpulan
&lt;/h2&gt;

&lt;p&gt;Dengan &lt;strong&gt;local proxy token‑bucket&lt;/strong&gt;, aplikasi Anda tidak lagi perlu khawatir soal &lt;code&gt;429&lt;/code&gt;. Proxy menyerap &lt;em&gt;burst&lt;/em&gt;, mengatur &lt;em&gt;throughput&lt;/em&gt;, dan mengurangi beban upstream lewat caching. Pola ini cocok untuk microservices, scraper, dan integrasi pihak ketiga.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Artikel ini dipublikasikan otomatis melalui pipeline Writer → Designer → Reviewer → Publisher.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>ratelimit</category>
      <category>proxy</category>
      <category>python</category>
    </item>
    <item>
      <title>Wiring a Third-Party Decision Model Into Claude Code, and What I Wouldn't Let It Do by Default</title>
      <dc:creator>Kiell Tampubolon</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:03:31 +0000</pubDate>
      <link>https://dev.to/kielltampubolon/wiring-a-third-party-decision-model-into-claude-code-and-what-i-wouldnt-let-it-do-by-default-1mho</link>
      <guid>https://dev.to/kielltampubolon/wiring-a-third-party-decision-model-into-claude-code-and-what-i-wouldnt-let-it-do-by-default-1mho</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloaded a vendor's "paste this into your agent" setup guide for Jev, a fast/cheap decision model. Had Claude Code build the integration live instead of just reading about it.&lt;/li&gt;
&lt;li&gt;Secured the real API key into gitignored storage before a single test call.&lt;/li&gt;
&lt;li&gt;Caught the docs-fetching tool hallucinating the API endpoint (doubled path, 404), fixed it with a stricter re-fetch.&lt;/li&gt;
&lt;li&gt;Built a reusable client, two on-demand skills, and a message-size router hook, pipe-tested through every branch, shipped off by default.&lt;/li&gt;
&lt;li&gt;Closed the loop by having Jev itself pick this article's title. 0.99 confidence, under a second, a fraction of a cent.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&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%2F1xg8b0c11jb5edfwj7os.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%2F1xg8b0c11jb5edfwj7os.png" alt="Manual triage versus Jev-assisted triage: a person opening 100 emails one by one, versus Jev sorting the same pile in seconds with three hot leads flagged and Claude writing only those replies" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I downloaded a setup guide for something called Jev: a model that promises "10x the power at 100x less cost" when you pair it with Claude. I didn't read it first. I opened Claude Code, told it to read the PDF, and asked for a plan I could run in that same session. What came back wasn't a plan to review later. It was a working integration, a real paid API call, a caught hallucination, and a question about how much of a vendor's own instructions I should let an agent follow without looking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Jev actually is
&lt;/h2&gt;

&lt;p&gt;Jev (&lt;code&gt;typesafe/jev-1.13&lt;/code&gt;, from TypeSafe AI) isn't a language model in the usual sense. It doesn't write anything. You send it a piece of text plus one or more typed questions, and it answers in one of three shapes: &lt;code&gt;choice&lt;/code&gt; (pick one option), &lt;code&gt;score&lt;/code&gt; (place it on a scale), or &lt;code&gt;noul&lt;/code&gt; (the probability a yes/no statement is true), each with a confidence number attached.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A fictional sales email, three questions, one real call through OpenRouter (a service that proxies API access to many AI models behind one key): sales, a 2.97-3 lead score at 0.97-1.0 confidence, 0.75-0.78 probability it needed a same-day reply. Under a second. About two cents per thousand calls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The guide's own benchmark backs this up at scale, run against 100 made-up emails for a fictional agency, recorded 21 September 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Lead score exactly right&lt;/th&gt;
&lt;th&gt;Hot leads found&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jev&lt;/td&gt;
&lt;td&gt;3.6s&lt;/td&gt;
&lt;td&gt;$0.0032&lt;/td&gt;
&lt;td&gt;89/100&lt;/td&gt;
&lt;td&gt;15/15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5 (thinking off)&lt;/td&gt;
&lt;td&gt;11.1s&lt;/td&gt;
&lt;td&gt;$0.061&lt;/td&gt;
&lt;td&gt;84/100&lt;/td&gt;
&lt;td&gt;15/15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Fable 5.1 (effort low)&lt;/td&gt;
&lt;td&gt;38.7s&lt;/td&gt;
&lt;td&gt;$0.76&lt;/td&gt;
&lt;td&gt;97/100&lt;/td&gt;
&lt;td&gt;15/15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fable was the most accurate. Jev beat Haiku on both speed and accuracy, at a fraction of the cost of either. None of them missed a hot lead. That's the actual pitch: Jev for volume, Claude for anything that needs writing or real reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the prompt before running it
&lt;/h2&gt;

&lt;p&gt;The guide's core step is a block literally labeled "copy-paste, the main prompt." Paste it into your agent and it asks for your OpenRouter API key, makes a live test call, and saves the setup as a reusable skill. The PDF contains text written to become my instructions the moment I follow the guide's own suggestion to paste it in.&lt;/p&gt;

&lt;p&gt;I didn't paste it in. I read the guide, pulled out what I actually needed, and wrote my own version instead of executing the vendor's copy verbatim. To be fair to it: the guide wasn't reckless. It tells the agent to ask before sending anything private, and its message-router feature defaults to off specifically because turning it on means every message passes through OpenRouter to TypeSafe. The point isn't that this guide was careless. It's that I didn't know that until I'd actually read it, and anything built to be pasted straight into an agent is worth that read every time, regardless of how it turns out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that happened in order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Secured the key first.&lt;/strong&gt; The real key arrived in plain text in chat. Before any test call, it went into a gitignored &lt;code&gt;.env&lt;/code&gt; file, with a backup encrypted into the personal notes repo's existing &lt;code&gt;SECRETS/&lt;/code&gt; convention (age ciphertext only, private key never committed). Only then did I make a live call. Secure it, then use it, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caught a hallucinated endpoint.&lt;/strong&gt; Jev launched after my training cutoff, so I fetched live docs instead of relying on memory. The first fetch described the API endpoint as &lt;code&gt;https://openrouter.ai/api/v1/api/alpha/decisions&lt;/code&gt;, base URL and path merged wrong. I called it. 404. A stricter re-fetch, asking for the path quoted verbatim instead of summarized, gave the real one: &lt;code&gt;POST https://openrouter.ai/api/alpha/decisions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built and pipe-tested before trusting it.&lt;/strong&gt; A Node client, two on-demand skills, and a message-size router wired as a real &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook, not just described in a doc.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The fix for a hallucinated endpoint isn't "don't trust docs-fetching tools." It's that a summarizer asked to describe an API reference page can merge a base URL with a path wrong, and the only way to catch it is to make the call and read the actual status code.&lt;/p&gt;
&lt;/blockquote&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%2Fgf90r0zymh49pv2vyxx5.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%2Fgf90r0zymh49pv2vyxx5.png" alt="The router hook's decision flow: message submitted, checked against router-on, slash-command or short-reply, and confidence at least 0.6, with every no routing to silent output and only a confident yes adding a note to context" width="800" height="863"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What got built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Node client&lt;/strong&gt; (&lt;code&gt;askJev({state, questions})&lt;/code&gt;) that calls the verified endpoint and returns the answer with its confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two on-demand skills&lt;/strong&gt;: one for calling Jev directly, one with ready-made recipes for sorting a pile of text (inbox leads, support-ticket urgency and churn risk, supplier-invoice fraud signals). Both carry the guide's own rule: under roughly 60% confidence, the item goes to a pile for a human to judge, never straight to action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A message-size router&lt;/strong&gt;, a real hook that fires on every message submitted to Claude Code, not something I only wrote about. Pipe-tested directly before trusting it: off, it produces no output at all. A slash command or short reply gets skipped before any call runs. A genuinely ambiguous message (0.38 confidence) gets classified internally but the note stays suppressed, a shaky verdict is worse than no verdict. A real self-contained task got sized correctly at 0.96 confidence, cost logged to a running total. It ships off by default, because the workspace it lives in holds personal and security-incident material I don't want leaving the machine by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest limit
&lt;/h2&gt;

&lt;p&gt;Claude Code doesn't have a real per-message model switch, and a hook can't create one. What the router actually does is add a note: this looks like a SONNET-sized job, or an OPUS-sized one. Acting on that note is still a judgment call, made by Claude or by me, not an automatic handoff. Calling it a "router" oversells it slightly, and I'd rather say that plainly than let the name imply more control than the mechanism has.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small, honest test of the whole thing
&lt;/h2&gt;

&lt;p&gt;Three titles for this article, and I didn't want to pick. So I had Jev decide, the same way it would decide between three email categories: one &lt;code&gt;choice&lt;/code&gt; question, three options, one call.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Picked at 0.99 confidence, for $0.000025, in under a second. The whole pitch in miniature: a cheap, fast, structured judgment call, made by something that only ever answers in multiple choice, never in prose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're wiring any brand-new third-party model into an agentic coding tool: read the vendor material yourself before letting an agent execute it verbatim, secure any credential the moment it exists, verify a live API against a real request and its actual status code rather than a summarized doc, and keep anything that touches every message you type off by default until you've decided you actually want it on.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>security</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
