惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
罗磊的独立博客
宝玉的分享
宝玉的分享
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
博客园_首页
量子位
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
S
SegmentFault 最新的问题
雷峰网
雷峰网
小众软件
小众软件
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
Jina AI
Jina AI
Hugging Face - Blog
Hugging Face - Blog

Echo JS

billboard.js 4.1.0: Live resizing, configurable subchart, React subpath & CSP-safe worker From 1,256ms to 96ms: Fixing INP in a Massive React Dropdown GitHub - evoluteur/cymatics: Play a frequency and watch the sand settle into its Chladni figure, computed from the wave equation. Memdeklaro - The Basics of Decentralized Identity (DID) and Self-Sovereign Identity (SSI) GitHub - evoluteur/platonic-solids: Turn the five Platonic solids in 3D, show their duals, read their measurements, and print the nets to fold your own. Sharing Application State in a URL GitHub - evoluteur/sacred-geometry: Sacred Geometry Generator: draw, tune, and export Vesica Piscis, Seed of Life, Flower of Life, Metatron's Cube, and the Golden Spiral as SVG Best of Self-Sovereign Identity: Digitalcourage, World Passport and Memdeklaro Reads Are Subscriptions - Migrating from Zustand to Coaction GitHub - evoluteur/binaural-beats: Simple web page to play binaural beats for sleep, meditation, relaxation, and focus: Delta, Theta, Alpha, Beta, and Gamma brainwave frequencies, with an optional pink or brown noise bed. toast-queue — Accessible, customizable toast notifications Building a High-Performance Data Grid in React, Vue, and Svelte I built a flight recorder for AI sessions React Authentication With JWT, Zustand, and Axios | JavaScript Tools Blog My idempotency library had one job. A dropped connection made it run the payment twice. "half-open" twice is not the same state: the bug that shaped breakwater 1.0 GitHub - evoluteur/evolutility-server-node: Framework for building REST APIs for CRUD with models rather than code (using Node.js, Express, and PostgreSQL). React Router v8 in Action: Lazy Loading and Nested Routes One $ for every environment | Xec My test suite had 100% coverage. Mutation testing still found real bugs. The type-safe data layer for Kysely | Kysera What JavaScript Obfuscation in the AI Era | JavaScript Tools Blog Using Mongoose Studio with Apache Cassandra via Data API GitHub - trekhleb/yesbrainer: 🧠 A council of AI models for the decisions that aren't no-brainers — they answer in parallel, debate to consensus, or get judged to a verdict. Browser-only, open source, bring your own keys (BYOK), no backend. Node.js has plenty of circuit breakers. So why did I build another one? My Redis library said the write succeeded. Redis was down. GitHub - Techthos/gadget: Prebuilt, interactive HTML widgets for MCP Apps in Go — data tables and forms, self-contained in a single binary, host-themed, spec-compliant. GitHub - evoluteur/react-morph-charts: React component for bubble chart, bar chart, and pie chart, with animated morphing transitions between charts, on hover, and on window resize. Interactive Metaballs Tutorial
How Railmid Works
6 September 2026 · four running demos · 2026-09-07 · via Echo JS

Cloudflare Durable Objects · netcode

How railmid
works

A browser 1v1 — and now 2v2 — running a 60Hz server-authoritative simulation inside one Cloudflare Durable Object. Four things are worth showing, and all four are things you have to watch rather than read.

62.5Hztick, measured in production

~17msperceived input latency

521Bone snapshot

300mslag compensation window

16arenas, one timer


The shape of it

One input, all the way round

The whole game lives in one Durable Object: sixteen arenas, one timer, and a simulation that owns no clock of its own. Here is what actually happens when you press a key — watch one input make the trip.

The round trip

running · one input, start to finish

Your champion moves on the press, not on the reply. The round trip still happens — it just stopped being something you wait for.

Everything below pump.ts is fed time rather than reading it. Match takes wall clock through advance(ms); the simulation itself imports nothing from the DOM, the network, or a clock, and has no randomness. That purity is why the same code runs on the server, in tests, and in the client's prediction path — and why moving it onto workerd took no polyfill and no shim.


Result 1 · the runtime

Alarms run this game at a sixth speed

A Durable Object needs something to drive it. Cloudflare's docs point at alarm(). We measured both, on real production infrastructure — and the difference is not subtle.

62.5Hz vs 10.76Hz

same movement · both measured in production

Both are the same simulation at the same speed. The bottom one is every tick a player would actually get from an alarm-driven loop.

The part that nearly shipped

Locally, wrangler dev reports alarms at 59.01Hz and intervals at 58.46Hz — alarms marginally ahead. The ranking inverts in production. Stopping at local numbers would have shipped the bottom lane.

Your development environment is a model of production, and models are wrong in the direction nobody checks.


Result 2 · the client

177ms → 17ms. Feel it.

A server-authoritative game has a problem: the server decides, and the server is far away. Wait for its reply and every keypress costs a round trip. The fix is to move immediately and correct later.

Below, both champions obey you. The left one waits for the server. The right one predicts. Hold the button and watch which one you'd rather play.

Hold to move

simulated 160ms round trip

or hold D / arrow key · release to stop

Prediction is the whole win. Everything else in the netcode — sending on keydown, 60Hz, a jitter-sized buffer — only shrinks the correction it has to make.

Result 3 · fairness

You shot where you saw them

Prediction fixes your champion. It does nothing for your opponent, who reaches you over the same network — so by the time you see them, they have already moved. Shoot at what is on your screen and, without help, you miss.

So the server keeps 18 ticks of pose history and rewinds. Your shot is judged against the world as you actually saw it.

Lag compensation

300ms window · rewind, resolve, return

Damage always applies at the present tick — you cannot retroactively un-kill something. Only the hit test moves.

What a rewind must refuse to smooth

Interpolating between two stored poses is a good approximation of a real path — walking covers about 3.4 units a tick. Then we shipped a 400-unit blink, and the interpolated midpoint became a place nobody has ever been. Rewound shots hit a phantom standing in the gap.

Continuous state interpolates. Discrete state — alive, shield raised, and now warped — never does.

Result 4 · the link

Which server answers is a coin toss

Cloudflare's edge is anycast: one address announced from about 330 sites, and BGP — not the browser and not us — settles which of them answers. It settles once per TCP connection and then holds for that connection's whole life. Measured from Los Angeles on 2026-08-31 over 46 real connections, the same player was terminated in Portland (~65ms), Miami (~152ms), Osaka (~222ms) and Sydney (~309ms) on consecutive attempts. A quarter of the draws were four times worse than the best on offer — on every frame, for the whole session.

So connecting is not one socket. The client opens one, pings it three times, takes the floor of the three, and either keeps it or holds it open and draws again — up to ten sockets or three seconds, and then the best one it drew, never the last one. Holding the losers is the whole design: with every earlier draw still open there is no stopping rule to get right.

The bar it draws against is yours rather than a constant — 1.3× the best round trip this browser has ever measured, seeded on a first visit by the floor the server tells the connection its geography allows. A fixed 100ms would make London burn all ten sockets on every connect to land exactly where its first draw already was; a fixed 200ms would cost Los Angeles 54ms by stopping at Miami and never finding Portland. There is no constant that is right for both. The memory decays by a tenth on a roll that met nothing, so a player whose old best has become unreachable does not roll ten sockets forever.

None of this is hidden while it happens: the status line reads finding a route · socket 2/10 · 152ms · want ≤130ms until it settles.

And if the link goes away mid-match

A socket that closes does not empty its seat. The server puts a stand-in in it — the champion simply stands still — and holds the seat, the name and the queue row for 15 seconds. The client comes back to the same address with a one-shot token, on a backoff bounded by that window, and resyncs the frame; it deliberately does not roll again, because swapping the socket under a running match is what loses the seat.

What it is not is a promise. A duel that reaches a winner rotates around a stand-in exactly as it rotates around a live loser, so a reconnect can be handed back its seat, a place in the queue, or neither. Fifteen seconds is a judgement about how long a match is worth pausing for, not a measurement.


The numbers

What we measured

Three things we have not settled, because a page that quotes the measurements without the gaps is selling rather than reporting: the longest production run was ten seconds, so sustained 62.5Hz is unverified. Hibernation was never exercised, and it is what decides the bill for an idle server. And the spike drove its loop over HTTP, so socket fan-out at rate is unmeasured.

One thing we did measure at scale surprised us. At around 300 connections, production began dropping sockets — and the failure was not that spectators saw less. It was the tick falling to 18Hz, so the two people actually playing took the match in slow motion. That is why the audience can never be allowed to raise the cap.

← All posts