Limits & Known Limitations
Item counts, item and connection size budgets, message size, deadlines and flow-control defaults for device capabilities, plus what is not implemented or validated yet
Limits & Known Limitations
Limits
Every server SDK runs the same Rust broker, so the protocol limits are the same everywhere. Only the memory budgets depend on the host.
| Limit | Value |
|---|---|
Items per gallery.pick / file.pick | 1–16 (maxCount) |
| Size of one uploaded item | 64 MiB (gallery.pick, file.pick, camera.capture, mic.record) |
Size of one file.save | 1 byte to 64 MiB |
| Upload bytes one connection may hold | 128 MiB on Node/Bun, Go, Kotlin, Swift and Rust; 16 MiB on Cloudflare Durable Objects |
| Upload bytes held across all connections | 1 GiB per process by default (configureDevice({ processRetainedBytes }) in TypeScript, AggregateRetainedBytes in Go, poolBytes in Kotlin, processRetainedBytes in Swift, aggregate_retained_bytes per DeviceServer in Rust). On Cloudflare, three connection budgets per Durable Object. |
| One device JSON message | 1 MiB, checked before parsing. Nesting depth is at most 32, and numbers must be integers of at most 2^53−1. |
| One binary frame | 64 KiB of payload, plus a 12-byte header |
| Default deadline | 300 s, clamped per capability: permission.query 30 s; pickers, file.save, permission.request and bluetooth.select 300 s; camera.capture, mic.record and bluetooth.scan 600 s |
| Client deadline ceiling | 300 s on the web host (maxTimeoutMs), 600 s on iOS by default |
| Lease | Renewed every 5 s, expires after 15 s without an acknowledged renewal |
| Default initial upload credit | 256 KiB for uploads, 64 events for JSON streams |
file.save credit window | At most 256 KiB outstanding, granted only after the user picks a destination |
| Recording length | maxDurationMs up to 600000 (10 min) for mic.record and camera video |
| Bluetooth filters | Up to 16 service UUIDs; namePrefix 1–64 characters |
| Background work | Not available in revision 1. At most 2 modules per connection may hold background work in the future. |
What happens when a limit is crossed:
- An item larger than 64 MiB, or larger than its declared size, ends the
request with
invalidParams. If the client declared the size up front, the server refuses before it allocates anything. Otherwise it enforces the limit as bytes arrive, because live recordings don't know their size in advance. - A full connection or process budget ends the request with
throttled. - A JSON message over the limits counts as a protocol violation on the connection. It's discarded, and repeated violations close the connection.
- Completed results count toward the budget until your handler returns. Copy what you need, for example upload it to storage, inside the handler.
On Cloudflare, one isolate (128 MB) serves every session in a Durable Object. That's why the per-connection budget is 16 MiB, and why a single item must also fit within 16 MiB there, even though the protocol allows 64 MiB.
The TypeScript RemoteServer also caps a single WebSocket message at 4 MiB
by default while the device plane is on (maxPayloadLength).
A host may enforce lower limits than the registry allows, for example a
smaller MaxItemBytes in Go, or maxDownloadBytes on the web host. Protocol
version 1 has no way to advertise lowered limits in advance, so a request
over a host's lower limit fails when it runs.
Known limitations
The protocol is version 1 and provisional. It won't be frozen until real-device validation is complete. Current gaps:
- Native hosts haven't run on physical devices. The iOS and Android device hosts are covered by unit and protocol tests with the platform faked. The iOS UIKit, AVFoundation and CoreBluetooth code is parse-checked only, and the Android adapters compile and lint clean. Real pickers, capture, recording, BLE and permission prompts still need on-device testing.
- Web validation used Chromium's fake media devices and emulated
Bluetooth. It hasn't been run with a physical microphone, camera or BLE
peripheral. Web
file.savehas unit tests only, with no real-browser test yet. bluetooth.scanis native-only. The web offersbluetooth.select(Chromium-based browsers only), but no scanning.- Bluetooth is identity-only.
bluetooth.selectreturns an id and a name. There is no connection or GATT access. mic.recordsupports only PCM16. There is no compressed audio format in revision 1.- No background lifetime. Every capability is activation-scoped, and work stops when the user leaves the screen.
- No resume. An operation interrupted by a disconnect fails with
connectionLostand has to be started again. - Desktop video has no audio track, and the desktop's camera, microphone, Bluetooth and OS dialogs have been tested with fake hardware and scripted UI only, not yet on real devices.
- Rust handlers get results asynchronously. Rust handlers are
synchronous, so a device call returns a
DeviceCallwhose result is applied later withthen(or read withon_settled/waitoff the handler thread), not awaited inline. - Cloudflare: the device path hasn't been run under
workerditself. Durable Objects stay resident while a device plane is live. - Built-in web clients have no device host. The client page that
RemoteServerserves, and the CloudflareserveClientbundle, don't attach aWebDeviceHost. Build your own client entry. - Lowered host limits can't be advertised, as described above.
- Go allowlist matching is exact. Go compares
AllowedOriginsstrings exactly, while the other SDKs normalize the scheme, host and default port first. List origins in their canonicalscheme://host[:port]form.
Consent & Security
How the device decides — host-owned consent, activity indicators, cooldowns and one-prompt-at-a-time, connection admission with Origin allowlists and authenticators, and what the server may and may not trust
Multi-Platform
What Hypen shares across platforms, and what stays platform-specific