Skip to content

Limits and timeouts

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 or the 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:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
import {
const API_DEADLINE_MS: 8000

apiPromise bounded by a deadline, for call sites that must not park forever.

apiPromise NEVER REJECTS: epochs.first settles only when a broker connection exists, so a broker frame that never bridges leaves it pending for the life of the realm. Awaiting it directly is correct wherever hanging is the honest answer, and wrong wherever the caller owns a socket, a timer or a UI that has to say something.

That distinction is not academic. In a WORKER realm the osra transport is {receive: self, emit: self}, which is inert until the page bridges it, so an unbridged worker parks every socket call here with no listening, no error and no rejection. The engine then reports a listener that neither succeeded nor failed, its reopen counters stay at 0 because reopen only runs from an error or close that never arrives, and the relay is never contacted at all. That state cost a long diagnosis: it presents as a transport fault and is invisible from every counter.

The latch mirrors storage.ts: once the broker has missed one deadline, later calls stop paying the full wait. net.ts needs it especially, because its listen path is bind('::').catch(() => bind('0.0.0.0')), so an unbounded-then-rejecting version would charge the deadline twice.

API_DEADLINE_MS
,
const apiWithin: (what: string) => Promise<{
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}>
apiWithin
,
class BrokerUnreachableError
BrokerUnreachableError
} from '@fkn/lib/api'
const API_DEADLINE_MS: 8000

apiPromise bounded by a deadline, for call sites that must not park forever.

apiPromise NEVER REJECTS: epochs.first settles only when a broker connection exists, so a broker frame that never bridges leaves it pending for the life of the realm. Awaiting it directly is correct wherever hanging is the honest answer, and wrong wherever the caller owns a socket, a timer or a UI that has to say something.

That distinction is not academic. In a WORKER realm the osra transport is {receive: self, emit: self}, which is inert until the page bridges it, so an unbridged worker parks every socket call here with no listening, no error and no rejection. The engine then reports a listener that neither succeeded nor failed, its reopen counters stay at 0 because reopen only runs from an error or close that never arrives, and the relay is never contacted at all. That state cost a long diagnosis: it presents as a transport fault and is invisible from every counter.

The latch mirrors storage.ts: once the broker has missed one deadline, later calls stop paying the full wait. net.ts needs it especially, because its listen path is bind('::').catch(() => bind('0.0.0.0')), so an unbounded-then-rejecting version would charge the deadline twice.

API_DEADLINE_MS
// 8000
try {
await
function apiWithin(what: string): Promise<{
cloud: {
fetch: (input: ProxyFetchInput, init: ProxyFetchInit) => Promise<Response>;
quota: () => Promise<{
overQuota: boolean;
remaining: number;
usedBytes: number;
limitBytes: number;
premium: boolean;
bytesPerSecond: number;
}>;
dns: {
lookup: (hostname: string, options?: {
all?: boolean | undefined;
family?: 0 | 4 | 6;
} | undefined) => Promise<{
address: string;
family: 0 | 4 | 6;
} | {
address: string;
family: 0 | 4 | 6;
}[] | undefined>;
};
fs: {
available: () => Promise<boolean>;
... 11 more ...;
setAdoptSource: (next: AdoptState | null, run: (() => Promise<void>) | null) => Promise<...>;
};
webvpn: {
...;
};
};
... 16 more ...;
hideInstallPrompt: () => Promise<...>;
}>
apiWithin
('the quota readout')
const {
const remainingBytes: number

bytes of free-tier volume left today

remainingBytes
} = await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.quota(): Promise<cloud.QuotaStatus>
export cloud_d_exports.quota
quota
()
const remainingBytes: number

bytes of free-tier volume left today

remainingBytes
// 5000000000 at the start of a UTC day, 0 once the free volume is spent
} catch (
var error: unknown
error
) {
if (!(
var error: unknown
error
instanceof
class BrokerUnreachableError
BrokerUnreachableError
)) throw
var error: unknown
error
var error: BrokerUnreachableError
error
.
Error.message: string
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.

These are the waits @fkn/lib pays on the way to its first call:

LimitValueWherePast it
apiPromiseno deadline@fkn/libnever rejects, so a call that awaits it waits for as long as the broker takes
API_DEADLINE_MS8,000 ms@fkn/libapiWithin rejects with BrokerUnreachableError
RETRY_API_DEADLINE_MS1,000 ms, after one miss@fkn/libevery later apiWithin waits this long instead, for the life of the realm
PARENT_ANSWER_MS2,000 ms@fkn/liba nested realm stops waiting for its parent’s port and mounts a broker frame of its own
COMMIT_TIMEOUT_MS10,000 ms, polled every 16 ms@fkn/libthe transport is handed out anyway

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:

LimitValueWherePast it
the root fetch backend decisionnone, read at the moment of the call@fkn/liban early call skips the extension and takes the next backend in the order backends sets, the cloud unless the desktop app is connected
waitForExtensionExposure1,000 ms, or 150 ms after load@fkn/libthe missing-extension handler runs, then the call rejects, see fetch()
POST_MAX_BODY_SIZE10,485,760 bytes of request body by defaultthe proxy413 request body exceeds POST_MAX_BODY_SIZE
CONNECT_TIMEOUT10,000 ms to reach the upstreamthe proxy502 upstream fetch failed: …
READ_TIMEOUT30,000 ms between two reads from the upstreamthe proxythe same 502 before the response head, and a failed body read after it, since the status is already out
caller request rateper tierthe proxy429 rate limit exceeded
shared budget per upstream originper originthe proxy429 upstream origin is saturated
COOLDOWN_MS60,000 ms for a proxy endpoint that failed a transport on a replayable bodythe brokerthe broker retries once on the next endpoint, and rethrows when there is none
ENDPOINT_TTL_MS30,000 ms for the proxy endpoint cachethe brokerre-derived afterwards
cloud.fetch, promptInstallno deadline@fkn/libwait on apiPromise

A deadline on the transfer itself is yours to set. init.signal is honoured on both paths:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const
const controller: AbortController
controller
= new
var AbortController: new () => AbortController

The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.

MDN Reference

AbortController
()
const
const timer: NodeJS.Timeout
timer
=
function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)
setTimeout
(() =>
const controller: AbortController
controller
.
AbortController.abort(reason?: any): void

The abort() method of the AbortController interface aborts an asynchronous operation before it has completed.

MDN Reference

abort
(new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('no answer in 10s')), 10_000)
try {
const
const response: Response
response
= await
(alias) namespace cloud
import cloud
cloud
.
cloud_d_exports.fetch(input: ProxyFetchInput, init?: ProxyFetchInit): Promise<Response>
export cloud_d_exports.fetch
fetch
('https://example.org/api/catalog.json', {
RequestInit.signal?: AbortSignal | null | undefined

An AbortSignal to set request's signal.

signal
:
const controller: 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.

MDN Reference

signal
}) // rejects once the timer fires
const response: Response
response
.
Response.status: number

The status read-only property of the Response interface contains the HTTP status codes of the response.

MDN Reference

status
// 200 from example.org, or 413, 429 or 502 from the proxy
} catch (
var error: unknown
error
) {
if (!
const controller: 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.

MDN Reference

signal
.
AbortSignal.aborted: boolean

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).

MDN Reference

aborted
) throw
var error: unknown
error
const controller: 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.

MDN Reference

signal
.
AbortSignal.reason: any

The reason read-only property returns a JavaScript value that indicates the abort reason.

MDN Reference

reason
// your Error('no answer in 10s'), the signal reached the proxy request
} finally {
function clearTimeout(timeout: NodeJS.Timeout | string | number | undefined): void (+1 overload)
clearTimeout
(
const timer: NodeJS.Timeout
timer
)
}

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()).

A socket is bounded in @fkn/lib, in the broker and at the relay, the far end of every net and dgram socket:

LimitValueWherePast it
broker connection for connect, listen, bind8,000 ms, then 1,000 ms@fkn/libBrokerUnreachableError on the error event
listen() with no hosttwo binds, '::' then '0.0.0.0'@fkn/libboth deadlines are paid, and the error names the second
CONNECT_ACK_TIMEOUT_MS12,000 ms for the relay to acknowledge a TCP connectthe brokeran error naming the timeout
DATA_STREAM_CLAIM_TIMEOUT_MS30,000 ms to claim the TCP data streamthe brokeran error naming the claim
socket.setTimeout(ms)yours, 0 disables@fkn/libemits timeout and nothing else
setKeepAlive initial delaywhole seconds, at least 1 for any positive delay, 0 otherwise@fkn/librounded
UDP_PORT_ACK_INTERVAL_BYTESan acknowledgement every 524,288 bytes consumed@fkn/lib@fkn/lib acks for you
UDP_PORT_MAX_UNACKED_BYTES8,388,608 bytesthe brokerinbound datagrams past it are dropped until an acknowledgement lands
UPLOAD_SHED_HIGH_WATER262,144 bytes of queued UDP uploadthe brokerfurther datagrams are dropped silently, and the send callback still reports queued
relay socket capacity, bind rulesthe relay’sthe relayrefused on error, and the message names the reason
dns.lookupno deadline, no cache@fkn/libwaits 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.

fs and opfs keep a clock, where cloud.fs has none. The account is the FKN identity a person carries between sites, and its caps apply to all three:

LimitValueWherePast it
PROBE_TIMEOUT_MS8,000 ms for the cloud availability probe, then 1,000 ms until a probe answers again@fkn/libthe hybrid fs reads the state as 'unknown' and queues instead of dropping
cloud.fs.*no deadline@fkn/libwait on apiPromise
in-memory flush debounce250 ms@fkn/libalso flushed on pagehide and on the tab going hidden
REHYDRATE_COOLDOWN_MS5,000 ms before an incomplete mount() is re-run@fkn/libremount() forces one
DRAIN_RETRY_MIN_MS60,000 ms between drain retries, doubling to 600,000 ms@fkn/libarmed only after a drain threw or left per-file failures
content type inference18 extensions@fkn/liban unknown extension sends no type, and the broker stores application/octet-stream
cloud.fs path1,024 characters, 64 segments, relative, no control character and no empty, . or .. segmentthe serviceInvalid path
.fkn and .fkn/*reservedthe brokerrefused
STORAGE_QUOTA_BYTES_FREE1,000,000,000 bytes per free account by defaultthe servicea write is refused
STORAGE_QUOTA_BYTES_PREMIUM100,000,000,000 bytes per premium account by defaultthe servicethe same refusal
STORAGE_MAX_OBJECTS10,000 objects per account by defaultthe servicea new path is refused
STORAGE_MAX_OBJECT_BYTES100,000,000 bytes per object by default, one PUTthe servicethe PUT is refused

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:

app.ts
import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
if (await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
fs_d_exports.available(): Promise<boolean>
export fs_d_exports.available
available
()) {
const {
const limitBytes: number
limitBytes
,
const remaining: number
remaining
,
const objects: number
objects
,
const maxObjects: number
maxObjects
} = await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
fs_d_exports.quota(): Promise<cloud.fs.StorageQuota>
export fs_d_exports.quota
quota
()
const limitBytes: number
limitBytes
// 1000000000 on a free account, 100000000000 on a premium one
const remaining: number
remaining
// what limitBytes has left, 0 once the account is full
const maxObjects: number
maxObjects
// 10000
const objects: number
objects
<
const maxObjects: 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:

LimitValueWherePast it
EXPOSURE_SAFETY_CAP_MS10,000 ms at most for the root attachFrame backend decision, 150 ms after the document is complete without the extension@fkn/libthe cloud backend is chosen
waitForExtensionExposure inside extension.attachFrame1,000 ms, or 150 ms after load@fkn/libthe install handler, unbounded by default as under fetch(), then the call rejects
HANDSHAKE_TIMEOUT_MS20,000 ms for the cloud handshake@fkn/librejects, and the iframe is restored
READY_TIMEOUT_MS65,000 ms for the render proxy to become ready@fkn/librejects
goto() on the extension30,000 msthe extensionrejects
goto() on the cloud backend30,000 ms in the render proxy, plus 5,000 ms in @fkn/libthe render proxy, @fkn/librejects at the first, or after the second when the render proxy never answers
a locator action, timeout option30,000 ms, retried every 50 ms@fkn/libthe last attempt’s error, No elements found for a missing element
domains entry253 characters, bare lowercase hostname@fkn/libdropped rather than repaired on the cloud backend
MAX_HOSTS16 hosts on the cloud consent cardthe brokerthe declared list is cut to 16, for the card and the stored grant alike
REDISPLAY_COOLDOWN_MS10,000 ms after a dismissed cloud consent cardthe brokerthe card is not shown again and frame.fetch fails closed
frame.fetch bodywhole, in one ArrayBuffer@fkn/libno streaming, no signal

The timeout option 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 {
const attachFrame: (options: AttachFrameOptions) => Promise<Frame>
attachFrame
} from '@fkn/lib'
const
const frame: Frame
frame
= await
function attachFrame(options: AttachFrameOptions): Promise<Frame>
attachFrame
({
iframe: HTMLIFrameElement
iframe
:
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
ParentNode.querySelector<"iframe">(selectors: "iframe"): HTMLIFrameElement | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('iframe')!,
domains?: string[] | undefined
domains
: ['example.org'] })
await
const frame: Frame
frame
.
function goto(url: string, options?: GotoOptions): Promise<void>
goto
('https://example.org/catalog')
const
const heading: string
heading
= await
const frame: Frame
frame
.
locator: (selector: string) => Locator$1<Extended<{
readonly element: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly getByRole: {
readonly to: "element";
readonly resolve: (context: LocatorContext, role: string) => Element[];
readonly render: (role: unknown) => {
fragment: string;
};
};
readonly getByText: {
readonly to: "element";
readonly resolve: (context: LocatorContext, text: string) => Element[];
readonly render: (text: unknown) => {
fragment: string;
};
};
readonly getByTestId: {
readonly to: "element";
readonly resolve: (context: LocatorContext, testId: string) => Element[];
readonly render: (testId: unknown) => {
fragment: string;
};
};
readonly first: {
readonly to: "element";
readonly resolve: (context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
};
};
readonly nth: {
readonly to: "element";
readonly resolve: (context: LocatorContext, index: number) => Element[];
readonly render: (index: unknown) => {
fragment: string;
};
};
};
readonly operations: {
readonly click: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly fill: {
readonly resolve: (context: LocatorContext, value: string, _options?: OperationOptions) => void;
};
readonly hover: {
readonly resolve: (context: LocatorContext, options?: PositionOptions$1) => void;
};
readonly textContent: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => string;
};
readonly getAttribute: {
readonly resolve: (context: LocatorContext, name: string, _options?: OperationOptions) => string | null;
};
readonly isVisible: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
readonly count: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => number;
};
readonly exists: {
readonly resolve: (context: LocatorContext, _options?: OperationOptions) => boolean;
};
};
};
readonly frame: {
readonly selectors: {
readonly locator: {
readonly to: "element";
readonly css: true;
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "descend";
};
};
readonly frameLocator: {
readonly to: "frame";
readonly css: true;
readonly barrier: "down";
readonly resolve: (context: LocatorContext, selector: string) => Element[];
readonly render: (selector: unknown) => {
fragment: string;
separator: "down";
};
};
readonly owner: {
readonly to: "frame";
readonly barrier: "up";
readonly resolve: (_context: LocatorContext) => Element[];
readonly render: () => {
fragment: string;
separator: "up";
};
};
};
readonly operations: {
readonly addStyleTag: {
readonly resolve: (context: LocatorContext, options: AddStyleTagOptions$1) => void;
};
readonly fetch: {
readonly kind: ChainKind;
readonly ...
locator
('h1').
textContent: (_options?: LocatorOptions | undefined) => Promise<string>
textContent
({
timeout?: number | undefined
timeout
: 5_000 })
const heading: string
heading
// 'Catalog', the text of the h1

Without an h1 the same call rejects with No elements found after 5 seconds.

A timeout rarely says timeout. The dispatch loop rethrows whatever the last attempt threw. 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.

More in frames and locators and actions.

The extension’s consent sheet and the activity log, the on-device record of what an app did, keep a few numbers of their own:

LimitValueWherePast it
severity0 to 3 in use, 4 declaredthe extension0 is granted silently and logged, 1 and up prompt
a once answerthe rest of the page loadthe extensionasked again on the next document
a session answeruntil the first extension start of a browser sessionthe extensiondeleted then
OCCLUSION_THRESHOLDthree consecutive misses of five hit-test points, checked every 200 msthe extensionsettled as deny once for every open item, so a retry asks again
the sheet’s read lock500 ms before Applythe extension
grouping by categoryat 6 rows or morethe extension
ACTIVITY_RETENTION_MS30 days, the newest 500 rows readthe extensionolder rows are deleted on every write
frame.fetch receipts for a failing callone per identical call per 60,000 ms windowthe extensionnot one per retry

None of this is reachable from an app. The sheet belongs to the user. The log is read from the extension’s own pages (see permissions and consent).

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:

LimitValueWherePast it
READY_TIMEOUT_MS30,000 ms for the package to post fkn-packages-readythe broker for connect, @fkn/lib for mountPackagesError code 'timeout', from connect or mount
HANDSHAKE_TIMEOUT_MS30,000 ms for the handshake over the port@fkn/libcode 'timeout'
PackageQuery.size1 to 100, default 36the brokerclamped
PackageQuery.text128 charactersthe brokercut
PackageQuery.type, id^[a-z0-9][a-z0-9-]{0,31}$the brokercode 'invalid', refused by name
PickOptions.title120 characters, rendered as textthe brokercut
protocol tag64 charactersthe brokercut
a source uri1 to 512 charactersthe brokercode 'invalid'
the sandbox origin label63 characters, which the encoding fills with roughly 40 characters of npm:<name>@<version>the brokercode 'unaddressable', so use a shorter package name
NOTICE_MS6,000 ms for the noConfirm noticethe brokerdismissed

The worst case for one connect() is a minute: 30 seconds to boot plus 30 for the handshake. An app that puts its own timer on a connect() can give up before the library does.

A size, text or title past its clamp is trimmed rather than refused. The answer shows the trim:

app.ts
import {
(alias) namespace packages
import packages
packages
} from '@fkn/lib'
const
const results: packages.PackageResult[]
results
= await
(alias) namespace packages
import packages
packages
.
packages_d_exports.search(query: packages.PackageQuery): Promise<packages.PackageResult[]>
export packages_d_exports.search

Search npm for FKN packages, e.g. search({ type: 'plugin', id: 'stub' }).

search
({
type: string

package kind, e.g. 'plugin' - becomes the keyword fkn-type:<type>

type
: 'plugin',
id?: string | undefined

host app scope, e.g. 'stub' - becomes the keyword fkn-<type>--<id>

id
: 'example',
text?: string | undefined

free text mixed into the registry query

text
: 'subtitles',
size?: number | undefined

result count, clamped to 1..100

size
: 500 })
const results: packages.PackageResult[]
results
.
Array<PackageResult>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
<= 100 // true, the broker clamps size on its way in
const results: packages.PackageResult[]
results
[0]?.
uri: string

normalized version-free uri, e.g. 'npm:@banou/stub-plugin-foo'

uri
// 'npm:@example/subtitles-plugin' for a hit, never pinned to a version

The broker clamps size silently, where a malformed type or id is refused by name. More in packages.

A room is bounded per member, per room and per address, every ceiling refuses by name, and none of it is metered:

LimitValueWherePast it
members per room64, and the members option clamps to 2 to 64the servicea join is refused rooms: the room is full
room.send text4,096 bytes of UTF-8, measured before the text is sealedthe brokerrefused rooms: the message is too large, whole, and never trimmed
the sealed message on the wire5,504 characters of ciphertext, inside a frame ceiling of 8,192 bytesthe servicethe same refusal with the room still open past the character cap, and a closed connection past the frame ceiling
per member10 messages per second, burst 20, and 65,536 bytes per secondthe servicerefused rooms: sending too fast, and ten refusals in ten seconds close the connection
per room100 messages per second, burst 200the servicethe same refusal, for whichever member sent past it
rooms held at once8 per account, 2 per address without onethe servicecreate is refused rooms: too many rooms
connections per member4, and 16 per addressthe servicerefused rooms: too many connections
silence from a membera ping every 25,000 ms, answered within 60,000 msthe servicethe connection is closed and the member enters the hold below
the hold after the last connection20,000 ms, with the broker re-dialing at 500 ms, 1 s, 2 s, 4 s and 8 sthe broker, the servicethe member is dropped, the room ends once nobody is left, and room.closed settles
blocks held by one room256the serviceblock is refused rooms: too many blocks, and nothing is evicted
rooms and connections the service holds at once500 rooms and 2,000 connections, per processthe servicecreate is refused rooms: too many rooms, a connection rooms: too many connections; nothing open is closed
bytes the service fans out8 MiB per second, per process, counted once per receiving connectionthe servicethe message is refused rooms: sending too fast before a seq is spent, and it counts toward nobody’s ten refusals
a member falling behind256 KiB buffered skips that message, 1 MiB closes the connectionthe servicea gap in seq for that member, then the hold
first frame after the socket opens10,000 msthe servicethe connection is closed and create or join rejects rooms: rooms are unavailable
a room parked by a deploy120,000 ms to be joined againthe servicethe invite answers rooms: no such room
metered volumenone: no room byte reaches cloud.quota(), on a free account or a premium onethe servicenothing; the rates above are the only ceiling

Two ceilings act on one message. The text cap is measured in your browser before the text is sealed and refuses by name, and the wire cap is measured by the service on the ciphertext, where a message past 5,504 characters is refused with the room still open and a frame past 8,192 bytes closes the connection. Nothing is trimmed by either. No room byte reaches cloud.quota(), so a premium account gets the same rooms as a free one, and the rates above are the only ceiling.

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:

LimitValueWherePast it
DAILY_QUOTA_BYTES, ACCOUNT_DAILY_QUOTA_BYTES5,000,000,000 bytes of free volume per UTC day, anonymous or signed inthe serviceoverQuota reads true and a free account is throttled
FREE_RATE_BYTES_PER_SEC10,485,760the servicebytesPerSecond under the free volume, which the relays and the proxy are meant to apply
THROTTLED_RATE_BYTES_PER_SEC1,048,576the servicebytesPerSecond once throttled, until UTC midnight
PREMIUM_RATE_BYTES_PER_SEC1,073,741,824the servicebytesPerSecond with a subscription, never throttled
cloud.quota() usedBytessaturates at limitBytesthe serviceconsumption past the free volume is not reported
CACHE_TTL5,000 ms for cloud.quota(), per caller, and for account.info(), shared by the whole broker documentthe brokera tighter poll costs nothing more
RECHECK_MS15 minutes between shell.updateReady() rechecks, and one on tab visibilitythe broker
shell.applyUpdate()waits up to 3,500 ms for the taken signalthe brokerreloads the broker frame, and answers false while a card is open or to a nested caller
ConnectButton iframe150 by 40 px@fkn/liboverridable through style

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).

The broker draws its cards through the broker frame. @fkn/lib clamps what it is told before it writes a clip:

LimitValueWherePast it
MAX_RECTS32 visible rects@fkn/libextra rects are dropped
MAX_HIDDEN8 hidden entries, each kind up to 40 characters@fkn/libdropped
MAX_INSET120 px of top inset strip@fkn/libcapped, and written as margin-top and --fkn-inset-top on <html>, both removed at zero
MAX_POLYGON_RECTS5 unrounded rects in one polygon clip@fkn/libmore rects, or any radius, use a path() clip
LINE_HEIGHT16 px per wheel line@fkn/lib
the frame’s z-index2147483647@fkn/lib
BAR_HEIGHT40 px of header barthe brokerpushed as the inset only in its push mode

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).