Blog › ICP guides

Felix developer on retainer: fiber concurrency, synchronous channel deadlock, Felix type system, and flx compiler on monthly retainer

September 26, 2026 · ~15 min read

A Felix concurrent processing pipeline was being built with two fibers communicating through a shared channel. The developer created a data_producer fiber and a data_consumer fiber, connected by a channel created with mk_channel(). In Felix, channels are synchronous: a write(c, value) call blocks the writing fiber until a matching read(c) is issued by another fiber, and a read(c) call blocks the reading fiber until a matching write(c, value) is issued. The rendezvous is bilateral — both fibers must be at the channel operation simultaneously for either to proceed. The developer structured the program so that both data_producer and data_consumer entered their main body and called read(shared_chan) first, expecting to receive initial configuration data from the other fiber. Since both fibers were attempting to read before either issued a write, both fibers blocked on their respective read calls waiting for a matching write that would never arrive. Neither fiber could advance to the write operation because neither had completed its read. Deadlock: 1 program run. The Felix developer on retainer diagnosed the synchronous channel ordering constraint: Felix channels do not buffer — they are not Go channels with a capacity queue; they are CSP-style synchronous rendezvous points where the write and read must occur simultaneously. With both fibers waiting to read, no fiber could proceed to write, and the program was stuck permanently. The developer restructured the communication: data_producer was changed to execute write(shared_chan, initial_config) first, then proceed to its main processing loop; data_consumer was left to read(shared_chan) first, then proceed. With the ordering established, the rendezvous completed and both fibers advanced. Deadlock: 1 → 0.

The work log entry read “fixed pipeline deadlock, 7h.” It names the result and duration. It cannot explain the semantic distinction between Felix’s synchronous channels and the buffered channels in languages like Go — a Go channel with capacity 1 allows a goroutine to send a value and continue without waiting for the receiver to be ready, because the value is stored in the buffer; a Felix write always blocks until a matching read is ready, because there is no buffer; the behavioral contract of Felix channels is the CSP rendezvous model, where communication is a simultaneous synchronization event rather than a producer-deposits-and-moves-on model; developers migrating from Go to Felix frequently write both-read-before-either-writes patterns because they are accustomed to channels that do not require this ordering discipline. It cannot explain why cooperative fiber scheduling in Felix means that a deadlock manifests as a program that simply stops making progress rather than as an error message — Felix fibers are scheduled cooperatively on a single OS thread; a fiber yields control either by reaching a channel operation (which transfers control to another fiber that can satisfy the rendezvous) or by calling yield explicitly; a deadlocked program produces no crash, no error, and no output; it simply halts at the point where both fibers are waiting at their channel operations with nothing to resolve the wait. It cannot explain Felix’s select primitive — select waits for the first of a set of channels to be ready for a read or write and dispatches on the ready channel; it is the Felix mechanism for non-deterministic multi-channel communication where the fiber’s next action depends on which channel becomes ready first; select introduces non-determinism into the fiber topology, which requires additional analysis to verify absence of livelock (where fibers perpetually select the same channel and starve others). The 7 hours of deadlock topology analysis, channel ordering restructuring, and fiber scheduling audit are invisible in the diff.

Felix fiber concurrency: spawn_fthread, synchronous channels, select non-determinism, and cooperative yield

Felix fibers are lightweight coroutines created with spawn_fthread(proc), which schedules the given procedure as a new fiber. The fiber scheduler in Felix is cooperative: fibers run on a single OS thread and yield control only at channel operations (read or write) or at explicit yield calls. A channel is created with var c = mk_channel[T]() where T is the type of values the channel carries. write(c, value) blocks the current fiber until a matching read(c) is called by another fiber; read(c) blocks until a matching write. The blocking is symmetric: neither the writer nor the reader can proceed independently; both must be at the channel operation simultaneously for the rendezvous to complete and both fibers to advance. This synchronous contract means that every channel pairing in a Felix program defines a producer-consumer ordering constraint: the fiber that writes must be ready to write before or simultaneously with the fiber that reads, or the writing fiber will block waiting for the reading fiber, and vice versa. A cycle in the write-before-read dependency graph produces a deadlock.

select provides non-deterministic multi-channel dispatch: select { read c1 => proc1; write c2 v2 => proc2; } waits for the first of c1 ready for reading or c2 ready for writing, then executes the corresponding branch. Select is the Felix mechanism for fibers that serve multiple channels without committing to a single one at a time. yield explicitly suspends the current fiber, transferring control to the scheduler for another fiber to run; it is useful for long-running computation fibers that need to cooperate with I/O fibers without natural channel-operation yield points. The gang construct runs a set of fibers on multiple OS threads, introducing true parallelism and requiring attention to data races at shared mutable state. Felix was designed by John Skaller with a focus on high-performance concurrent systems and compiles to C++ via the flxg compiler. Its closest retainer neighbors are Erlang developer retainers (both are concurrency-first languages with lightweight process/fiber primitives and message-passing communication) and OCaml developer retainers (both compile to native code via an intermediate representation and have strong type systems with algebraic sum types), but Felix’s synchronous CSP-style channels where write and read must rendezvous (rather than Erlang’s mailbox model or Go’s buffered channels), its cooperative single-thread scheduler default, and the flx compiler pipeline that generates C++ make the retainer work distinct in deadlock topology analysis, synchronous rendezvous ordering design, and fiber-to-C++ boundary work.

Felix type system: sum and product types, record types, polymorphic functions, and pattern matching

Felix’s type system is built around sum and product types as first-class constructs. A product type is written a * b for a pair, or (a, b, c) for a triple; values are constructed with tuple syntax (val_a, val_b) and deconstructed with pattern matching or fst/snd projection. A sum type is written a + b (also known as a discriminated union or tagged union); a value is of one of the two types and carries a tag indicating which; pattern matching on a sum type must cover all variants for the match to be exhaustive. Record types use {x: int; y: int} syntax for named-field structures; fields are accessed with .field projection. Function types are written a -> b; functions are first-class values and can be passed to and returned from other functions. Polymorphic functions use type parameters: fun identity[T](x: T): T => x; defines a function polymorphic in T; Felix infers the type parameter from the call site argument type.

Pattern matching in Felix uses match expr with | pattern1 => body1 | pattern2 => body2 endmatch syntax; patterns can match product type components, sum type variants, record fields, and literal values. The match compiler verifies exhaustiveness: a match that does not cover all constructors of a sum type produces a compile-time error. Primitive types include int, double, bool, string, and unit (the zero-element product type, analogous to () in ML). Felix compiles to C++ and provides facilities for C interop: include "header.hpp" imports C++ headers; extern "c" fun c_function: int -> int = "c_function" binds a C function to a Felix name; link "libname" adds a linker flag. The flx script.flx command executes a Felix script without explicit compilation; flx build script.flx produces a binary; flx --run script.flx combines compilation and execution. The --static flag produces a statically linked binary.

How HourTab tracks Felix developer retainer hours

Felix retainer work carries the invisible-hours problem specific to synchronous concurrency: a deadlock in a Felix fiber topology produces no error, no stack trace, and no message — the program simply stops making progress, which in a server context looks like a hang and in a batch context looks like an infinite wait. The deadlock described above — where both data_producer and data_consumer called read(shared_chan) before either called write, causing both to block indefinitely — is the most common correctness issue in Felix code written by developers familiar with Go’s goroutine and buffered channel model. In Go, a send to a buffered channel with available capacity returns immediately, so two goroutines that both send before receiving will succeed if the buffer is large enough; in Felix, a write always blocks until a matching read is available, so the same code structure produces an immediate deadlock with no buffer to absorb the mismatch. Diagnosing a Felix deadlock requires reconstructing the channel dependency graph: for each channel, which fiber writes and which reads; for each fiber, what is the sequence of channel operations before any other work is done; a cycle in the write-before-read dependency graph indicates a potential deadlock. In a program with 5–10 fibers and multiple channels, this graph reconstruction is a multi-hour investigation with no visible artifact in the codebase. A retainer engagement typically involves a full channel topology audit (all channels enumerated with their writers and readers; dependency graph drawn and checked for cycles), a select review (all select statements analyzed for non-determinism correctness and starvation absence), and a gang assessment (any use of multi-OS-thread fiber scheduling reviewed for shared mutable state and potential data races).

HourTab gives Felix developers a public retainer-hours URL they send to clients — typically high-performance computing teams using Felix for concurrent pipeline architectures, researchers building concurrent algorithms in a CSP-style framework, and systems programmers using Felix’s C++ compilation target for embedded concurrent systems. For Felix retainers, each work log entry should name the mechanism (concurrency category: synchronous channel rendezvous ordering, write-before-read dependency cycle, select multi-channel non-determinism, yield cooperative preemption, gang multi-thread scheduling; type category: sum type variant dispatch, product type tuple construction, record field projection, polymorphic function instantiation; specific fiber names and channel names and before/after deadlock count). Felix retainers are often compared to Erlang developer retainers for the lightweight process/fiber and message-passing framing, but Felix’s synchronous CSP-style rendezvous channels (where write and read must be simultaneously ready, rather than Erlang’s asynchronous mailbox model where a send always succeeds and the receiver processes messages from its queue at its own pace), the cooperative single-thread scheduler that makes deadlocks manifest as silent hangs rather than deadlock detection errors, and the C++ compilation target that enables embedding Felix concurrency in C++ systems make the retainer work distinct in channel rendezvous ordering analysis, cooperative yield point placement, and fiber topology deadlock auditing. HourTab’s work log makes the channel dependency graph analysis, synchronous rendezvous ordering restructuring, and fiber topology deadlock diagnosis visible to clients who would otherwise see only the symptom — a pipeline that silently stops producing output — and not understand why the fix required knowing that Felix channels require a write-before-read ordering discipline that buffered-channel languages do not enforce, and that the silent hang was both fibers perpetually waiting for a rendezvous that only the other could initiate.

Track Felix developer retainer hours without the status emails

HourTab gives Felix developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your concurrency audit log — synchronous channel deadlock diagnosis, fiber topology ordering, select non-determinism analysis — becomes the proof of value that gets the retainer renewed.

See HourTab pricing →

FAQ: Felix developer retainers

What does a Felix developer on retainer typically do?

A Felix developer on monthly retainer covers Felix fiber concurrency (spawn_fthread fiber creation; mk_channel() synchronous channel; write(c, value) blocks until matching read; read(c) blocks until matching write; synchronous rendezvous — both must be ready simultaneously; select non-deterministic multi-channel first-ready dispatch; yield cooperative yield; cooperative scheduling on single OS thread; gang multi-OS-thread parallelism), Felix type system (int/double/bool/string/unit primitives; a * b product type; a + b sum type; {x: int; y: int} record type; a -> b function type; fun f[T] polymorphic function; match/case exhaustive pattern matching), and Felix compilation and C++ interop (flx script.flx execution; flx build compilation; --static linking; Felix compiles to C++ via flxg; include "header.hpp"; extern "c" fun C binding; link "libname" linker flag).

What Felix work is most commonly underlogged in a retainer?

Synchronous channel deadlock diagnosis (developer spawned two fibers; both fibers called read before either called write; Felix channels are synchronous — read blocks until matching write; neither fiber could advance; deadlock: 1/program run; restructured with explicit first-write-then-read ordering; deadlock: 1/run → 0; 5–9 hrs invisible); fiber communication topology design (identifying which fiber should write first; designing channel graphs without write-before-read cycles; 4–8 hrs invisible); select non-determinism analysis (select dispatches on first ready channel; correctness when multiple channels may be ready; fairness and starvation analysis; 4–7 hrs invisible); C++ interop boundary work (extern C binding; type mapping; include header paths; link directive; 3–6 hrs invisible).

What are typical Felix developer retainer rates?

Entry-level Felix developers (1–2 years, basic fiber spawn, channel creation, flx compiler workflow) bill at $60–$110/hr. Mid-level Felix programmers (2–4 years, synchronous channel semantics, deadlock diagnosis, select non-determinism, sum and product types) bill at $95–$170/hr. Senior Felix concurrent developers (4–8 years, complex fiber topologies, gang multi-thread scheduling, C++ interop design, performance-critical concurrent algorithms) bill at $140–$250/hr. Monthly retainer ranges: $2,000–$4,000/mo advisory (15–25 hrs), $5,600–$14,000/mo for full Felix concurrent systems development.

What should a Felix developer retainer agreement include?

A Felix developer retainer agreement should specify: concurrency scope (spawn_fthread fiber creation; mk_channel channel creation; write/read synchronous rendezvous; select non-deterministic dispatch; yield cooperative yield; gang multi-thread; deadlock topology analysis); type system scope (int/double/bool/string/unit; a * b product; a + b sum; {x: int; y: int} record; a -> b function; fun f[T] polymorphic; match/case); C++ interop scope (include header; extern C binding; link directive; type mapping); compilation scope (flx execution; flx build; --static; flxg C++ backend); and hour logging format (concurrency: channel deadlock, write ordering, select topology; type: sum dispatch, record field, polymorphic instantiation; specific fiber and channel names and before/after deadlock count).

How should Felix developer retainer hours be logged?

Log each Felix retainer session with: concurrency category (deadlock: both-fiber-read-before-write, synchronous rendezvous, write ordering restructure, channel dependency cycle; channel: mk_channel, write blocks until read, read blocks until write, bidirectional vs unidirectional; select: multi-channel first-ready dispatch, starvation analysis; yield: cooperative preemption points, gang multi-thread handoff); type category (product: a * b construction, pattern match, projection; sum: a + b variant, exhaustive match/case; record: field declaration, .field projection; polymorphic: fun f[T] instantiation); the specific fiber names and channel names and before/after deadlock count (fiber: data_producer and data_consumer; channel: shared_chan = mk_channel(); both called read(shared_chan) before either called write; synchronous channel blocked both; deadlock: 1/run; data_producer restructured to write(shared_chan, config) first; deadlock: 1/run → 0); and the before/after metric. Include whether fix required write-before-read ordering restructure, splitting to two unidirectional channels, or replacing blocking channel with select for non-deterministic readiness.