Skip to main content
Most transfers just work. When one does not, Floe usually tells you why. Find the message you are looking at below, or use the page search. If you have no message at all and the transfer simply never starts, begin with The transfer never starts.

In the browser

”Open in your browser”

Floe is running inside another app’s built-in browser, and those handle file transfers poorly. Follow the two steps on screen, or tap Copy link to paste in browser and open it in Chrome, Safari, Edge, or Firefox. Continue anyway (some features may not work) proceeds regardless. Until you do one of those, the page has not joined the room, so nothing is lost by switching. See Receiving files.

The transfer never starts

Both sides have to be present at the same time, and the connection is made directly between them, so several things can hold it up.
  • The sender left. Closing the browser tab, quitting Floe Desktop, or stopping floe send ends the session. Nothing resumes it.
  • The recipient has not actually opened the link. In the browser and the desktop app the transfer then starts by itself. floe receive asks first, so a sender can sit on “Connecting” while the recipient has not answered the Accept? [Y/n] prompt. Pass -y to skip it.
  • The network will not allow a direct path, and the relay is not available. In the browser this is the sender’s Network relay fallback checkbox, on by default. A browser recipient has no such control, so ask the sender. On the CLI the equivalent is not passing --no-relay. Floe Desktop has no relay-fallback control at all, so the relay is always available there.
The card reads “This link is either expired, already in use, or does not exist.” A room takes exactly one recipient, so the most common cause is that someone is already connected on this link, including you in another tab. Opening a link does not use it up, so closing the other tab frees the seat. Otherwise ask the sender for a new link.

”Could not connect. Ask the sender to enable “Network Relay” and try again.”

This is the receiver’s message for every failure Floe treats as expected, so the relay is not always the culprit. The sender may have turned their Network relay fallback checkbox off, run floe send --no-relay, or simply closed their tab.

”Connection failed. Enable “Network Relay” to connect across restrictive networks.”

The sender’s half of the same situation. The only available path was relayed and relay fallback was off, so Floe closed the connection rather than sending over a route you excluded.

”Transfer blocked. Relay limit exceeded.”

The connection ended up relayed and the queued files total more than 2 GB. Nothing was sent. Remove files until the total is at or under the cap, or get a direct connection, which has no limit: use a normal home or personal network, disconnect from any VPN, avoid strict corporate and university networks, or try a personal hotspot. Exactly 2 GB is allowed; only more is refused. See File size limits.

”Peer disconnected. Waiting for reconnection”

The other side dropped before the first file arrived. It does not resume by itself. If your network blipped before any file landed, Floe rejoins the room on its own and the sender builds a fresh connection. Once files have started landing, signaling is finished and Floe stops rejoining, so a drop after that point ends the session.

”Too many refreshes. Reconnecting”

You have hit the per-IP connection limit: 30 new connections per minute. Wait a minute. This is usually rapid page reloads, or several devices behind one address. On a self-hosted instance raise MAX_CONNECTIONS_PER_IP.

”Transfer timed out waiting for receiver. Please try again.”

The sender announced a file and the other side never acknowledged it within two minutes. Usually the recipient closed the tab, or their connection died between connecting and receiving.

”Could not prepare files for ZIP.” or “ZIP creation failed.”

Both suggest Download All instead, and that is the right move. Zipping holds every file in memory at once, so a large multi-file transfer can exhaust a phone or a tab. Saving files from their own rows uses far less memory.

Downloads behave oddly on iOS

Use Download ZIP when there is more than one file. Saving them individually behaves inconsistently in Safari and iOS web views. The two bulk buttons appear only for multi-file transfers, and they come and go: they show as soon as the second file lands, hide while the next file streams, and settle once the transfer finishes. A single file is saved from the download button on its own row.

A file did not appear at all

Floe compares what arrived against the size the sender announced and discards anything that does not match, in either direction, rather than handing you a file that looks whole and is not. Ask the sender to send that file again.

On the command line

code "olive-tiger-castle" not found or expired (codes expire after 10 minutes)

The code was mistyped, or it has passed its ten minutes. Codes are lowercase words joined by hyphens, but capitalization does not matter: floe receive and Floe Desktop lowercase what you type before the lookup, so Olive-Tiger-Castle resolves the same as olive-tiger-castle. Surrounding spaces are trimmed and capitalization is folded for you; nothing else is. Ask the sender to start a new session for a fresh code.

this code is no longer active; ask for a new one

The code resolved, but nobody is sharing on it. The sender finished, quit, or their session ended, and joining the empty room they left behind is what produces this message. A word code stays valid for its full ten minutes, and a pasted share link does not expire at all. Either way it now points at a room nobody is in, so ask for a new one.

room is full (someone else may already be receiving)

Someone else joined this transfer first. A room holds exactly two peers. Ask for a new code.

timed out establishing a connection

Thirty seconds passed without a usable path. Either the other side never joined, or neither side could reach the other and no relay was available. If you passed --no-relay, this is what that looks like when no direct path exists: the flag removes the relay from the address list, so there is nothing else to try and the failure is this generic timeout rather than a message about the relay. --relay-only used to fail the same way against a server with no TURN relay. It no longer reaches this timeout: the flag is checked against the server’s connection list before the transfer starts, and the command stops at once naming the flag. See --relay-only needs a TURN relay.

--relay-only needs a TURN relay

The full line names the server it asked: --relay-only needs a TURN relay and https://floe.example.com offers none; drop the flag, or configure a relay on the server. --relay-only routes everything through the relay and never tries a direct path, so a server that offers no relay leaves it nowhere to go. The check runs before the transfer starts, so this arrives immediately instead of as a timeout half a minute later. Drop the flag (and unset FLOE_RELAY_ONLY if you set it), or add a relay to the instance. Floe’s own server always has one, so this points at a self-hosted address. See TURN relay. A second wording, the connection details from ... could not be read, means the server was never heard from, so floe fell back to public STUN and cannot say what the server offers. Check --server for a typo, check that a reverse proxy forwards /api/, or try again in a minute: that endpoint rate-limits repeated requests from one address.

connected, but no data arrived from the sender within 30s

You reached the sender and they never started sending. Ask them to try again.

connection closed while waiting for the receiver (transfer declined or receiver exited)

The other side answered n at the Accept? [Y/n] prompt, or their process ended.

timed out waiting for delivery confirmation from peer

Every byte was sent, but for a full minute the receiver neither confirmed delivery nor took in any more of the data still on its way. The connection has most likely stalled. Check with the recipient before resending.

relay connections are capped at 2 GB

The same cap as in the browser, from the sending side. See File size limits.

Cannot transfer: your floe is too old for this peer

Also worded Cannot transfer: peer's floe is too old. Peers on different Floe versions normally transfer fine, so a small version difference is never the cause. This appears only when a release changed the transfer protocol in a way the two versions cannot bridge. The message prints both sides as You: and Peer: with their version strings. Update the older one:
Terminal
Every current peer works out the correct side itself, from the protocol numbers rather than from the words the other side sent, so the remedy you see is the one for your own surface. A peer older than that convention can still show you the other side’s wording; in that case go by the version strings. Refresh the page to update a browser, and update Floe Desktop through the Microsoft Store or by reinstalling from the download page.

Cannot update a dev build.

The binary was built from source or installed with go install, so it carries no version to compare and cannot replace itself. Install through a package manager or the install script for a release build with working self-update. See Install the CLI.

installed via homebrew - run 'brew upgrade floe' to update

floe update refuses to replace a binary your package manager owns. Use the command it names. Detection is a substring match on the binary’s path for homebrew, cellar, linuxbrew, scoop, or winget, so a script install living under a directory with one of those words in it is treated the same way.

Warning: could not reach signaling server for TURN credentials. Using STUN only.

Floe could not fetch relay credentials and is carrying on with direct connections only. On a restrictive network the transfer will probably fail. Check the server address, and check FLOE_SERVER in your environment if you did not pass --server.

no files to send

Every path you gave resolved to nothing, or to an empty folder. Floe checks before touching the network.

Slow to connect when a VPN or virtual machine is running

A VPN such as Tailscale, or virtualization software such as VMware, VirtualBox, WSL, or Hyper-V, adds network interfaces. The CLI probes each one, so a machine with several can spend a long time on “Connecting…” before settling. The transfer still completes, and the browser is unaffected. Floe already ignores link-local addresses, which covers dead auto-configured adapters. A VPN adapter with a routable address, such as Tailscale’s 100.x range, is not filtered. Pin the CLI to the interface you mean:
Terminal
Repeat the flag or separate names with commas for several. It is an allowlist rather than a preference, so remove it again when you change networks, or the connection fails outright instead of merely being slow.

A browser recipient cannot join a CLI transfer

A browser can only join by opening the link, never by typing a code. Share the link that floe send prints with browser recipients, and the code with CLI or desktop recipients, both of which also accept the link.

In Floe Desktop

Every failure in the app is shown with an Error: prefix, so a message on screen reads Error: That code was not recognized. ....

”That code was not recognized. Check it for typos, or ask the sender for a new one.”

The same causes as the CLI’s expired-code message: a typo, or more than ten minutes have passed. It also appears when the code belongs to a different server from the one the app is set to.

”Connected, but the sender never started sending. Ask them to try again.”

Floe reached the sender and waited thirty seconds with nothing arriving.

”Could not reach the server. Check your internet connection.”

The app could not reach the signaling server at all, or the server never assigned it a role within twenty seconds. If you recently changed Server address, check it with Test.

The Test button says the realtime connection was refused

“The server answered, but the realtime connection was refused. If it is behind a reverse proxy, check that /ws is being forwarded.” The address is a Floe signaling server but the WebSocket upgrade at /ws is not reaching it, which is nearly always a proxy that forwards ordinary requests without forwarding the upgrade. See Run behind one domain.

The Test button says the API answered with an HTTP error

“The server is running, but its API answered with HTTP 404.” /api/ is not being forwarded. Fix it before relying on the server, because nothing else reports this failure: relay credentials fall back to public STUN and the room code silently fails to register, so you get a share link with no code and no error.

The Test button says the server has no TURN relay

“Connected. This server has no TURN relay, so Hide my IP will not work.” The address is a working Floe signaling server, and this is a pass rather than a failure, which is why it carries no Error: prefix. Hide my IP address cannot work at all without a relay, and ordinary transfers succeed only where the two devices can reach each other directly. Turn the switch off, or add a relay to the instance. See TURN relay.

”Hide my IP needs a TURN relay and this server has none.”

The server answered, and its answer had no relay in it. The transfer stops before it starts rather than timing out about thirty seconds later with advice about both devices being online. Turn Hide my IP off, or add a relay to your instance. Floe’s own server always has one, so this only appears on a self-hosted address. Test reports the same thing about an address before you rely on it. See TURN relay.

”Hide my IP needs a TURN relay, and this server’s connection details could not be read.”

The difference from the message above is who is being blamed. Here the server was not heard from at all, so Floe fell back to public STUN and cannot say what the server offers. Check the Server address for a typo, run Test against it, and if it is behind a reverse proxy check that /api/ is being forwarded. Trying again in a minute also helps, because the TURN endpoint rate-limits repeated requests from one address. Turning Hide my IP off lets the transfer proceed over whatever path is available.

The Test button says it may be the web app rather than the signaling server

Something answered but not with a health check. In a default self-hosted setup the web app is on port 3000 and the signaling server on 3001, so http://localhost:3000 produces this and http://localhost:3001 is what you want. Server address always wants the signaling server. Test never follows redirects, so a captive portal or a catch-all rewrite cannot pass itself off as a healthy server.

There is no right-click menu setting

The Windows section, and with it Show in right-click menu, is hidden in the Microsoft Store build. A packaged app cannot register the entry in a way File Explorer would read, and asking for it returns “the right-click menu is not available in the Microsoft Store build”. Install the build from GitHub if you need it.

The switch for the right-click menu keeps turning itself off

The registry write was refused, which usually means a managed or locked-down machine.

Every transfer is capped at 2 GB

Hide my IP address routes every transfer through the relay, which is where the cap lives, so it applies even on a network that could have connected directly. An oversized send fails with “Turn off Hide my IP to send larger files”. See Hide my IP address.

Floe will not start

If the Microsoft Edge WebView2 runtime is missing and the machine cannot reach Microsoft’s download, Floe cannot start and says so. Install WebView2 yourself, then start Floe again. See Install the desktop app.

On a self-hosted instance

The browser cannot connect to my signaling server

Set the signaling address and recreate the container. No rebuild is needed, because the client reads it at runtime:
Terminal
Compose passes NEXT_PUBLIC_SOCKET_URL into the container as SOCKET_URL, which is the name the running client reads. Outside Compose, set SOCKET_URL on the client process. Setting NEXT_PUBLIC_SOCKET_URL on an already-built client does nothing, because Next inlines that name at build time. Check what the client is actually serving:
Terminal
An empty socketUrl means the browser will use its own origin, which is correct only when both services sit behind one reverse proxy. Otherwise confirm the URL is reachable from your users’ browsers: http://localhost:3001 only works when the browser is on the same machine as the server.

Browser requests are blocked, and the server answers HTTP 500

A rejected CORS origin does not come back as a recognizable CORS error. The server answers 500 with {"error":"Internal server error"} and logs the stack, while the browser reports a generic CORS failure. If you see that pairing, check CLIENT_URL first. It must match your client’s public origin exactly, including the scheme and with no trailing slash, and it holds one origin. A comma-separated list matches nothing. https://floe.one, https://www.floe.one, and http://localhost:3000 are always allowed in addition.

429, or “Rate limit exceeded”

Four independent per-IP limiters, each over a 60-second window: 30 connections shared across Socket.IO and WebSocket (MAX_CONNECTIONS_PER_IP), 20 TURN credential requests (MAX_TURN_REQUESTS_PER_IP), 60 room-code requests (MAX_CODE_REQUESTS_PER_IP), and 60 stats reports, which is the one limit with no environment override. The three HTTP limiters answer 429 with Too many requests, or Too many reports for stats. The connection limiter is not HTTP: it refuses the Socket.IO handshake and closes the WebSocket with the reason Rate limit exceeded. Two things to know before raising a limit. Only admitted connections count: a refused connection attempt is not recorded, so a client that keeps retrying gets back in once its admitted connections age out of the 60-second window, the same way the three HTTP limiters recover after a 429. And setting any MAX_* variable to 0 does not disable the limit; it silently restores the default. Behind a reverse proxy, set TRUSTED_PROXY_COUNT to the number of proxy hops so the server reads real client IPs instead of the proxy’s. The server defaults to 1, but the Compose stack publishes ports directly and passes 0, so a Compose deployment behind a proxy has to set it.

503 “Server busy, try again shortly”

POST /api/code returns this once MAX_ACTIVE_CODES codes are live at once, regardless of source IP. It bounds the memory the code registry can use. Only expiry frees a slot: resolving a code does not delete it, so a code used immediately still holds its slot for the full ten minutes.

The TURN relay is not working

Check which option your instance uses. Managed Cloudflare TURN. Confirm both CLOUDFLARE_TURN_KEY_ID and CLOUDFLARE_TURN_KEY_API_TOKEN are set, then:
Terminal
A working setup returns two entries: a stun:stun.cloudflare.com:3478 one with no credentials, and a turn:/turns: one that carries a username and credential. If it returns Google STUN servers instead, one of the two variables is missing or the API token is invalid. Credentials are cached in memory for 5 minutes. Both variables are read once at startup, so a fixed key takes effect only after a restart. A key that breaks later is masked for a while: the server keeps serving the last good copy for up to about 23 hours after it was minted before it falls back, and logs Cloudflare TURN mint failed with a reason word (such as TimeoutError or status 401) at most once every 5 minutes. Self-hosted coturn. The bundled service uses host networking, so it needs a Linux host with a public IP, a domain, and TLS certificates. Confirm TURN_SECRET and TURN_DOMAIN match coturn’s static-auth-secret and realm. Setting only one of the pair falls through to STUN with no warning.

coturn logs Default realm: localdomain

coturn is running on built-in defaults with no auth secret, and relaying silently fails for exactly the peers it exists to serve. This happens when coturn/turnserver.conf did not exist before the first docker compose up, so Docker created a directory at that path. Run rmdir coturn/turnserver.conf first. Copying a file onto a directory succeeds and writes it inside, so the obvious fix appears to work and does not. Then fetch the example config and restart. See TURN relay.

The server logs nothing at all

That is expected. The signaling server has no access log and no startup banner, not even a “listening on” line, so docker logs on a healthy server is empty. Use GET /health, which returns {"status":"healthy","uptime":<seconds>}, to confirm it is alive and to see whether a restart landed.

docker compose ps never shows the client as healthy

The client image ships no healthcheck, so it has no health state to report. Only the server does.

Still stuck

Open an issue on GitHub with your browser and operating system, whether the connection was direct or relayed, and the exact message you saw. For Floe Desktop, Settings has a Copy button under About that puts the app version, the protocol version, and the server on your clipboard. Those are the details worth including.