Stop throttling signed media and ease the browser's e621 queue

Signed media URLs are fetched by <img>/<video> tags without an
Authorization header, so they were charged to the anonymous 120/min
bucket: past that, galleries and the fish-greeting download got 429 JSON
instead of image bytes. The raw/thumbnail/staged-file/similarity-file
actions are now exempt, and THROTTLE_ENABLED=false removes the general
anon+user limits for private/tailnet deployments (login/register/proxy
guards stay).

The SPA's e621 client also stops self-throttling so hard: 1s gap between
browsing calls (2.5s for the stricter IQDB endpoint) and a 15s cooldown
instead of 60s when e621 answers 429.
This commit is contained in:
2026-09-22 22:32:37 -05:00
parent 474403ffe2
commit e2697c0a78
7 changed files with 82 additions and 18 deletions
+12 -9
View File
@@ -89,18 +89,19 @@ export function effectiveCredentials(
const GIT_HASH = typeof __GIT_HASH__ === "string" ? __GIT_HASH__ : "dev";
const CLIENT_VERSION = `J621/${GIT_HASH} (JakeBreath)`;
// e621 allows 2 requests/second hard, 1/second sustained — and the IQDB
// endpoint is stricter, so stay comfortably under it. Serialize every request
// through a queue with a minimum gap.
// e621 allows 2 requests/second hard, 1/second sustained. Serialize every
// request through a queue with a minimum gap; IQDB is throttled much harder
// by e621, so it gets a wider gap of its own.
let lastRequestAt = 0;
let queue: Promise<unknown> = Promise.resolve();
/** A hung request would block the whole serialized queue forever. */
const REQUEST_TIMEOUT_MS = 20_000;
const REQUEST_GAP_MS = 1500;
const REQUEST_GAP_MS = 1000;
const IQDB_GAP_MS = 2500;
/** A 429 (or a CORS-blocked failure) pauses every e621 call for a while. */
const RATE_LIMIT_COOLDOWN_MS = 60_000;
/** A 429 (or a CORS-blocked failure) pauses every e621 call briefly. */
const RATE_LIMIT_COOLDOWN_MS = 15_000;
const COOLDOWN_KEY = "j621.e621.cooldown";
let cooldownUntil = 0;
@@ -143,10 +144,10 @@ function schedule<T>(task: () => Promise<T>): Promise<T> {
return run;
}
async function throttle(): Promise<void> {
async function throttle(minGap = REQUEST_GAP_MS): Promise<void> {
const wait = Math.max(
0,
lastRequestAt + REQUEST_GAP_MS - Date.now(),
lastRequestAt + minGap - Date.now(),
e621CooldownRemainingMs(),
);
if (wait > 0) {
@@ -168,7 +169,9 @@ export function e621Request<T>(
options: E621RequestOptions = {},
): Promise<T> {
return schedule(async () => {
await throttle();
await throttle(
path.includes("iqdb_queries") ? IQDB_GAP_MS : REQUEST_GAP_MS,
);
const base = credentials.base_url.replace(/\/+$/, "");
const url = new URL(`${base}/${path.replace(/^\/+/, "")}`);