Every wait in @fkn/lib either has a deadline or is deliberately unbounded. Every cap is applied by one named part of the platform: @fkn/lib, the broker, the extension, the proxy, the relay, the render proxy, the service or the rooms service. This page lists them by surface, each with its value, where it is applied and what happens past it, so you can promise the right thing in a UI and tell a timeout from a refusal.
Only one of these numbers is exported as a value: API_DEADLINE_MS, the deadline for reaching the broker. The broker is the connection your app holds into FKN, and @fkn/lib reaches it through the broker frame, a hidden fkn.app iframe it mounts. Every call that needs the broker waits for that connection first. You can observe the deadline on a socket call or on an apiWithin of your own:
The deadline of apiWithin, for call sites that own a socket, a timer or a UI and must not park.
apiPromise NEVER REJECTS: a broker frame that never bridges leaves it pending for the realm's
life. In a worker the transport {receive: self, emit: self} is inert until the page bridges it,
so an unbridged worker parks every socket call with no listening, error or rejection, which
presents as a transport fault invisible from every counter.
Once one deadline is missed, later calls wait only the retry deadline (as account-storage.ts does):
net.ts listens with bind('::').catch(() => bind('0.0.0.0')) and would otherwise pay it twice.
The deadline of apiWithin, for call sites that own a socket, a timer or a UI and must not park.
apiPromise NEVER REJECTS: a broker frame that never bridges leaves it pending for the realm's
life. In a worker the transport {receive: self, emit: self} is inert until the page bridges it,
so an unbridged worker parks every socket call with no listening, error or rejection, which
presents as a transport fault invisible from every counter.
Once one deadline is missed, later calls wait only the retry deadline (as account-storage.ts does):
net.ts listens with bind('::').catch(() => bind('0.0.0.0')) and would otherwise pay it twice.
message// '@fkn/lib: no broker connection within 8000ms, so the quota readout could not be requested'
}
apiWithin bounds only the wait for the broker, so the call you wanted follows it in the same try. Every other error is rethrown, because a storage or permission failure is not a missing broker. The 8000ms in the message holds until the first miss. After that, every later rejection says 1000ms.
Every other number is a constant inside one of those parts, and the third column of each table says which one.
The extension is the FKN browser extension. The proxy is what cloud.fetch sends a request through. The relay holds the real socket at the far end of net and dgram, and the render proxy is the cloud frame backend. The service is the FKN server that keeps account files and meters cloud egress, and the rooms service holds each room and relays its messages.
net.connect, Server.listen and dgram.bind go through apiWithin. http inherits the same deadline through the net.Socket that every request opens. These are the calls that give up on a missing broker.
The rest of the library waits on apiPromise with no deadline. A few calls answer at once in a realm that has no window (a realm is one JavaScript execution context, such as a window or a worker). connection and lifecycle says which calls those are. The storage availability probe has a deadline pair of its own and answers rather than rejects (see storage).
The retry deadline is a latch. Once any apiWithin deadline has been missed, the short one applies to every later call, even after a broker shows up.
A backend is where a call runs: the extension, the cloud or the desktop app. The root fetch picks a backend at the moment of the call, and extension.fetch and cloud.fetch bound different things. Most of these numbers belong to the proxy, since cloud.fetch sends every request through it:
The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.
status// 200 from example.org, or 413, 429 or 502 from the proxy
} catch (
var error:unknown
error) {
if (!
constcontroller:AbortController
controller.
AbortController.signal: AbortSignal
The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.
The aborted read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (true) or not (false).
The signal read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.
The status is the upstream’s when the proxy reached it, and the proxy’s own when it did not. A refusal is a Response carrying one of the statuses above and a JSON body { "error": "…" }, so read the body to tell the two apart (see fetch()). A stall after the response head has no status left to carry it, so the body read throws instead.
The 1,000 ms on waitForExtensionExposure bounds only the wait for the marker the extension sets. Past that the missing-extension handler runs. The default handler opens the broker’s install card and waits on the person with no deadline, so extension.fetch, extension.attachFrame and the root fetch with credentials: 'include' are unbounded out of the box.
setMissingExtensionHandler(null) removes the handler. A missing extension then rejects as soon as the marker wait is over (see fetch()).
waits on apiPromise, and a name with no answer resolves undefined, or [] with all: true
The transport underneath is WebTransport when the realm has it and the setup completes, and WebSocket otherwise. One relay session is shared by every socket in a data plane, the shared worker behind the broker document. A lost session closes every socket on it with the same error. The next connect, listen or bind dials again (see TCP and UDP sockets).
A UDP send callback means the datagram was handed to the transport, never that it was delivered, so the upload cap above is invisible from the callback. The relay’s own limits come back as the error event’s message: non-public targets, ports it will not bind, and capacity. All of them are listed under limitations.
cloud.fs sends the path as given, so /library/catalog.json reaches the service with an empty first segment and is refused. Under fs and opfs the same path is not refused (see storage). The account totals are what cloud.fs.quota() reports. They cover every app of the account, while the files themselves stay isolated per app:
limitBytes// 1000000000 on a free account, 100000000000 on a premium one
constremaining:number
remaining// what limitBytes has left, 0 once the account is full
constmaxObjects:number
maxObjects// 10000
constobjects:number
objects<
constmaxObjects:number
maxObjects// true while a new path can still be created
}
available() answers whether the broker holds a connect token. It answers false rather than rejecting when there is none (see storage). Without a token, every cloud.fs call that reaches the service, quota() included, rejects.
The first hybrid call on a page whose broker never answers can take 8 seconds, because mount() lists and a listing probes availability. cloud.fs in the same situation waits forever.
flush() has no deadline and promises nothing. It catches every backing failure and resolves. The evidence of trouble is pending() growing (see storage).
A frame’s calls run on one of two backends, the extension or the render proxy, the cloud frame backend. Every wait on the way to a working frame has a number, on both:
Limit
Value
Where
Past it
EXPOSURE_SAFETY_CAP_MS
10,000 ms at most for the root attachFrame on cookies: 'native' to wait for the extension, 150 ms after the document is complete without it; 'persistent' and 'ephemeral' do not wait
2,000 ms for the extension’s worker to hear of the window an attach on cookies: 'native' opened, matched on the calling frame and the url; a window it heard of more than 10,000 ms before (CREATED_TARGET_LIFETIME_MS) is never taken, and two attaches at once take the oldest each
the extension, from its first store release after 0.1.54
The timeout option, on a locator action and on goto(), is the only one of these you set per call. The consent sheet is what the extension shows a user before an action above severity 0. The sheet is raised before the timer starts, so consent never counts against the timeout:
app.ts
import {
constattachFrame:AttachFrameFunction
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
Attaches FKN to an iframe the app mounted, or with { window } opens the attachment in a window
of its own. cookies picks the jar and with it the backend (AttachCookies): 'persistent', the
default, and 'ephemeral' run on the cloud render proxy whatever is installed, and never wait for
the extension; 'native' runs on the extension, waiting for it to expose itself while the page
loads (at most 10000 ms) and showing the install prompt when it does not. A blank attach is the
cloud render proxy's alone, so beside 'native' it is a TypeError. frame.backend() says which
backend serves an attachment.
A window opens before the first await, so call this directly in the click or key handler whose
activation opens it. On 'persistent' or 'ephemeral' it opens on the cloud, extension or not. On
'native' it opens as a real browser window on the extension, with no exposure wait; an extension
that does not announce attachWindow, or none at all, is refused at once with
ExtensionOperationUnsupportedError (operation 'attachWindow'), before anything opens or is
waited for, so the same click can still open one on another value.
attachFrame({
iframe: HTMLIFrameElement
iframe:
var document:Document
window.document returns a reference to the document contained in the window.
Navigates the frame to url, which must pass the rules an iframe src does, and adds its host
to the attachment. Resolves at options.waitUntil, and rejects with TimeoutError past
options.timeout (GotoOptions).
Every deadline ends as a TimeoutError, exported from the root, and its message is what the call reported at that moment: the last attempt’s error for a locator action, kept as cause. The timeout message appears only when no attempt failed before the deadline.
The cloud frame.fetch rows cover only what @fkn/lib and the broker do before the call reaches the render proxy. Under the default addressing, the render proxy’s shell refuses it outright.
A package is an npm module FKN loads on a sandbox origin of its own. Two clocks bound a connection to one, and a set of clamps decides what a query and a uri may carry:
Limit
Value
Where
Past it
READY_TIMEOUT_MS
30,000 ms for the package to post fkn-packages-ready
262,144 characters of sealed message, nonce and ciphertext in base64url, which carries at most 196,580 bytes of UTF-8, unless the owner set another with limit
10,000 ms for the join every broker connection sends first, and 30,000 ms from open for a connection whose first frame is manage, which only fkn.app sends, from the Rooms tab and the room limit card, and which the room closes once it has answered
claim waits while fkn.app shows the person their claimed rooms with an Unclaim on each, resolves once an Unclaim frees the slot, and is refused rooms: too many claims when they close the card
a claim’s description
200 characters after trimming, as String.length counts them, and at least 1: one line of plain text, no control character, line or paragraph separator, bidi control or lone surrogate
500,000,000 bytes, each message counted at its wire size plus 64, or the lower size limit the account set in the Rooms tab of its fkn.app settings, from 1,000,000 bytes, which mailbox.cap reads after usage(); a claim that keeps no messages stores nothing and has no size limit
the rooms service
send, and an edit that makes a message larger, are refused rooms: the mailbox is full, and nothing is dropped
the owner’s storage, while the mailbox is on
the account’s storage quota, files, mailboxes and stored objects together, asked again within a minute of the mailbox changing or of a write it refused; a claim that keeps no messages counts 0 bytes and is never refused for it
200 messages or 65,536 bytes, whichever comes first and never fewer than one message, and limit asks for fewer
the rooms service
more reads true, and the next page starts after last
an idle mailbox
30 days without a write, and reads do not count
the rooms service
the mailbox moves to archive storage, mailbox.archived reads true, and the next write, edit or delete brings it back first
a lapsed claim
checked every 24 hours, and kept 30 days after premium ends, with an email 7 days in
the service, the rooms service
the claim and its mailbox, if it keeps one, are deleted, and the room becomes an ordinary one that ends once nobody holds a seat
metered volume
a free account’s or a guest’s room bytes, in both directions, count toward the same 5,000,000,000 bytes per UTC day as cloud egress, reported every 60,000 ms, and a premium account’s count toward nothing
Two checks act on one message. The broker seals the text and measures the sealed form against room.self.maxMessageBytes before it sends, so the refusal costs no round trip, and the rooms service measures it again on arrival. Nothing is trimmed by either. Room bytes on a free account or a guest share the daily volume that cloud.quota() reports, and a premium account’s are not metered at all.
1,000 per free account and 5,000 per premium one, in an hour that opens with the first upload it counts; a put refused for its arguments or its account, or by the object cap, the quota or this cap as it starts, does not count, and any upload past those checks does, even one the store fails, one aborted and one refused when it completes
its sealed size, the file plus 28 bytes for every 1 MiB record and 28 bytes for an empty file, from the start of the upload until the delete, against the account’s storage quota
until it is deleted, or the account is: nothing expires
the service
not applicable
Reads have no limit of their own. They go from the fkn.app frame to cdn.fkn.app, are not counted, and do not touch the daily volume that cloud.quota() reports. The rest is on object storage.
The service meters cloud egress and the broker caches the readout. The shell is the FKN surface that can update and reload the page, and it keeps a clock of its own:
Limit
Value
Where
Past it
DAILY_QUOTA_BYTES, ACCOUNT_DAILY_QUOTA_BYTES
5,000,000,000 bytes of free volume per UTC day, anonymous or signed in
the service
overQuota reads true and a free account is throttled
FREE_RATE_BYTES_PER_SEC
10,485,760
the service
bytesPerSecond under the free volume, which the relays and the proxy are meant to apply
Only cloud egress counts: the relays behind net, dgram and http, and the proxy behind cloud.fetch. Extension traffic is the browser’s own and is never metered here. Key your own meter on throttled, because overQuota and a saturated usedBytes are true for a premium account too (see account and quota).
An app meets these numbers in two places. The first is the inset strip, where a fixed element that sets top: var(--fkn-inset-top, 0px) stays clear of the bar. The second is the capturing wheel listener. While two or more rects show, it scrolls the nearest scrollable ancestor under the pointer itself and cancels the event, so the notch lands once (see how it works).