HypenHypen
Device Capabilities

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.

LimitValue
Items per gallery.pick / file.pick1–16 (maxCount)
Size of one uploaded item64 MiB (gallery.pick, file.pick, camera.capture, mic.record)
Size of one file.save1 byte to 64 MiB
Upload bytes one connection may hold128 MiB on Node/Bun, Go, Kotlin, Swift and Rust; 16 MiB on Cloudflare Durable Objects
Upload bytes held across all connections1 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 message1 MiB, checked before parsing. Nesting depth is at most 32, and numbers must be integers of at most 2^53−1.
One binary frame64 KiB of payload, plus a 12-byte header
Default deadline300 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 ceiling300 s on the web host (maxTimeoutMs), 600 s on iOS by default
LeaseRenewed every 5 s, expires after 15 s without an acknowledged renewal
Default initial upload credit256 KiB for uploads, 64 events for JSON streams
file.save credit windowAt most 256 KiB outstanding, granted only after the user picks a destination
Recording lengthmaxDurationMs up to 600000 (10 min) for mic.record and camera video
Bluetooth filtersUp to 16 service UUIDs; namePrefix 1–64 characters
Background workNot 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.save has unit tests only, with no real-browser test yet.
  • bluetooth.scan is native-only. The web offers bluetooth.select (Chromium-based browsers only), but no scanning.
  • Bluetooth is identity-only. bluetooth.select returns an id and a name. There is no connection or GATT access.
  • mic.record supports 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 connectionLost and 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 DeviceCall whose result is applied later with then (or read with on_settled / wait off the handler thread), not awaited inline.
  • Cloudflare: the device path hasn't been run under workerd itself. Durable Objects stay resident while a device plane is live.
  • Built-in web clients have no device host. The client page that RemoteServer serves, and the Cloudflare serveClient bundle, don't attach a WebDeviceHost. Build your own client entry.
  • Lowered host limits can't be advertised, as described above.
  • Go allowlist matching is exact. Go compares AllowedOrigins strings exactly, while the other SDKs normalize the scheme, host and default port first. List origins in their canonical scheme://host[:port] form.