RubyMine Remote Development: Architecture, Boundaries, and Failure Recovery
RubyMine's Remote Development splits the IDE into a local UI frontend and a remote language backend over SSH. Covers trust boundaries, data flows, heartbeat protocol, failure recovery, and conditions—LSP adoption, browser frontends, zero-trust networks—that would force a redesign.
14 Nov 2025, 06:54 UTC

The Problem: Full IDE Fidelity on Remote-Only Code
Developers increasingly work on code that must execute in a specific Linux environment—containers, VMs, or bare-metal servers—while expecting the same completion, navigation, refactoring, and debugging experience they get locally. Installing Ruby, Rails, gems, and native extensions on every laptop is impractical; worse, it creates drift between development and production. RubyMine's Remote Development solves this by running the language-aware backend on the target host and keeping only a thin UI frontend on the developer's machine.
Smallest Suitable Design: Split Frontend/Backend over SSH
JetBrains' Remote Development protocol divides the IDE into two processes:
- Local frontend — renders the editor, project tree, tool windows, and handles keystrokes. It contains no language logic, no index, no build tools.
- Remote backend — runs on the target host (via SSH), loads the project, builds indexes, runs the Ruby type checker, executes tests, and drives the debugger.
Communication uses a custom binary protocol tunneled through the SSH connection with automatic port forwarding. The frontend opens a single SSH session; the backend is downloaded, unpacked, and started automatically on first connect. No VPN, no extra daemons, no exposed ports beyond SSH.
Trust Boundary: Source Stays Remote
The fundamental trust decision: source code never leaves the remote host unless the editor explicitly requests a viewport range. The local process holds only the lines currently visible (plus a small buffer for scrolling). Authentication relies entirely on SSH keys—no IDE-specific credentials, no token stores, no cloud accounts. If the SSH key is compromised, the attacker gains shell access; the IDE protocol adds no additional attack surface.
This differs from classic "remote filesystem" mounts (SSHFS, NFS) where the local IDE reads every file to build its own index. Here, the index lives remotely, built by the same backend that powers completion.
Data Boundary: What Crosses the Wire
| Stays Remote | Syncs Locally |
|---|---|
| Project indexes, PSI caches, stubs | Editor viewport content (visible lines + context) |
| .idea/ workspace metadata (run configs, VCS roots) | User preferences (keymap, theme, inspection profiles) |
| Gem bundles, node_modules, compiled assets | Unsaved changes buffer (local until explicit save) |
| Git object database | Git status for project tree decoration |
Large binary artifacts—gems with native extensions, webpack output, Docker image layers—never traverse the SSH tunnel. This keeps bandwidth low and latency predictable even on slow links.
Operational Checks: Heartbeats and Health Signals
The protocol emits a heartbeat ping every 5 seconds. If three consecutive pings fail, the frontend marks the connection stale and switches the editor to read-only mode with an offline indicator in the status bar. Reconnection uses exponential backoff (2s, 4s, 8s, max 60s).
Backend health surfaces in the IDE status bar:
- Indexing progress (percentage, files remaining)
- Heap usage (remote JVM heap, not local)
- Active debugger sessions
For deeper diagnosis, open Help → Diagnostic Tools → Log in Files on the local side; the remote backend writes its own idea.log under ~/.cache/JetBrains/RemoteDev-/log/ on the target host.
Failure Modes and Recovery
Network Partition
When the SSH tunnel drops, the frontend immediately goes read-only. Keystrokes accumulate in a local unsaved-changes buffer (in memory, not disk). On reconnect, the backend sends the current remote file state. If the remote file changed while disconnected, a last-write-wins merge occurs with a user prompt for divergent files—similar to Git's merge conflict UI but at the file level.
Backend Crash
If the remote JVM crashes (OOM, segfault in JNI, killed by OOM killer), the frontend detects the broken pipe and attempts a backend restart. Persisted indexes survive in ~/.cache/JetBrains/RemoteDev-/index/, so restart typically takes seconds, not minutes. In-flight debugger sessions are lost; breakpoints persist.
Sync Conflict Example
# Scenario: Developer edits app/models/user.rb locally while offline.
# Meanwhile, CI pipeline runs rubocop --auto-correct on the same file remotely.
# On reconnect:
# 1. Backend sends current remote content.
# 2. Frontend detects local buffer differs from both base and remote.
# 3. Dialog shows three-way diff: base (last known synced), remote (CI), local (your edits).
# 4. Choose: keep local, take remote, or manual merge in built-in diff tool.
Conditions That Would Change the Design
- LSP adoption for Ruby — If a mature Ruby Language Server (e.g., Ruby LSP, Sorbet LSP) reaches feature parity with JetBrains' proprietary backend, the remote backend could shrink to a thin LSP client, reducing memory footprint and enabling polyglot backends.
- WebSocket transport for browser IDEs — JetBrains Gateway and Projector already experiment with WebSocket-based frontends. A browser-based RubyMine would replace the local native frontend with a WASM/JS editor, keeping the same remote backend protocol.
- Zero-trust network mandates — Environments requiring mutual TLS (mTLS) instead of SSH keys would need a new transport layer; the current SSH-only design assumes key-based auth is sufficient.
Practical Verification Checklist
- In RubyMine:
File → Remote Development → New Connection → SSH. Enter host, user, key path. Watch the "Downloading backend" → "Starting backend" → "Indexing project" sequence. - Create a Rails project on the remote host (
rails new demo --skip-bundle). Confirm code completion works forApplicationControllerwithout any local Ruby installation. - Set a breakpoint in a controller, start
Debug → rails server. Verify the debugger pauses, variables inspect, and stepping works. - Disconnect network (Wi-Fi off /
sudo ip link set eth0 down). Type in the editor—observe read-only banner and local buffer. Reconnect; if you edited a file that also changed remotely, verify the conflict dialog appears. - Check remote logs:
ssh user@host 'tail -f ~/.cache/JetBrains/RemoteDev-*/log/idea.log'.
Limitations to Plan For
- Initial indexing on large monorepos (100k+ files) can take 5–15 minutes and spike remote CPU/RAM. Provision the remote host with at least 4 vCPU / 16 GB RAM for typical Rails apps.
- Latency ceiling: typing feels local up to ~150 ms RTT. Beyond that, the local buffer masks latency but completion popup flicker becomes noticeable.
- File watchers (Guard, Spring, Listen gem) run remotely. If the project lives on a network filesystem (NFS, CIFS) mounted into the remote host,
inotifyevents may not fire—configure polling fallback. - Debugger port forwarding:
byebug/pry-byebugdefault to port 1234. If that port is used locally, configureDEBUGGER_PORT=26162in the remote run configuration and ensure the SSH tunnel forwards it.
When to Re-evaluate
Revisit this architecture if your organization adopts a zero-trust overlay (Tailscale, WireGuard with mTLS) that prohibits raw SSH, or if Ruby LSP matures enough to replace the proprietary indexer. Until then, the SSH-backed split frontend/backend remains the smallest design that delivers full IDE fidelity on remote-only code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.