Blog › ICP guides
Pony developer on retainer: reference capabilities, actor model, ORCA garbage collector, behaviour design, and Pony systems programming on monthly retainer
October 21, 2026 · ~18 min read
A high-throughput data aggregation system built in Pony had been producing five data corruptions per day. The system used an actor that received updates from three concurrent producer actors and maintained a shared aggregation object. The Pony developer on retainer diagnosed the root cause: the shared aggregation object had type annotation ref, which permits mutable access but does not enforce unique ownership. In Pony’s reference capability system, a ref type allows the object to be aliased — multiple variables in the same actor can reference it — but it is not sendable across actor boundaries. The problem was not that the object was shared across actors directly (which the Pony compiler would have rejected); the problem was that a mutable member within the aggregation object was being aliased through a box-typed accessor that was then used as the basis for updates from a callback behaviour. When the callback ran on the aggregation actor, the box-aliased field was being mutated through two paths simultaneously: the primary update method and the callback. Changing the aggregation object’s type to iso (uniquely owned, no external aliases permitted) and restructuring all updates to go through a single be update(data: Data iso) behaviour using consume data to transfer ownership eliminated the aliasing path. Data corruptions: 5 per day → 0.
The work log entry read “fixed data corruption in aggregation actor, 14h.” It names the symptom and the duration. It cannot explain to a client why the fix required understanding Pony’s reference capability lattice, why a ref-typed object can be corrupted through aliased box access in a single-actor context, why the fix was an iso capability annotation plus consume at each transfer point rather than a lock or barrier, why Pony has no runtime race detector (because its type system guarantees at compile time that races cannot occur in correctly-typed programs, and this was a case where the incorrect capability annotation masked the alias), or why 14 hours of capability subtype analysis was a better investment than a mutex wrapper. The diagnosis required understanding the full capability subtype lattice: iso (unique: read and write, no external aliases, sendable); val (globally immutable: read, no write, sendable, infinitely aliasable); ref (mutable: read and write, aliasable within one actor, not sendable); box (read-only view: read but no write, aliasable, not sendable); tag (opaque: no read or write, just identity, sendable, infinitely aliasable); trn (write-unique: write, may be aliased as box, transitional toward val). The capability annotation error, the viewpoint adaptation path that produced the aliased write, the consume restructuring — none of these have artifacts proportional to their complexity in the committed diff. The corruptions: gone. The compile-time guarantee: race-free.
Pony’s reference capability system: the lattice, subtyping, viewpoint adaptation, and consume
Pony’s reference capability system is the compile-time mechanism that eliminates data races without locks or runtime synchronization. Every object reference in Pony has a reference capability that specifies two things: what the holder of the reference is allowed to do with the object (read, write, or neither), and what guarantees are provided about other references to the same object (no other mutable aliases exist, all references are read-only, etc.). The six capabilities form a subtype lattice: iso is the most restrictive (exactly one reference exists; that reference can read and write; it can be sent to another actor via consume); val is globally immutable (any number of val references to the same object may exist across all actors; none can write; all can read; val is sendable); ref allows mutable aliasing within a single actor (any number of ref references in the same actor can read and write; no references to the same object exist in other actors; not sendable); box provides a read-only view (any number of box references can exist in any actor; none can write; the underlying object may be ref-mutable from another alias in the same actor; not sendable); tag provides opaque identity access only (no read or write; can be sent to any actor; used as actor references, since Pony actors are inherently tag from outside); trn (transition) allows write access while permitting box aliases during initialization, with the intent of transitioning to val once the object is fully initialized.
Capability subtyping: iso is a subtype of all capabilities (an iso reference can be used where a less capable reference is expected). The subtype ordering is: iso <: trn <: ref <: box <: tag and iso <: val <: box <: tag. This means an iso reference can be passed to a method expecting a ref, box, val, or tag receiver; but a ref reference cannot be used where a val is expected (because ref allows mutation, violating the val immutability guarantee). Viewpoint adaptation is the rule for determining the capability of a field when accessed through a reference of a given capability. If you hold a ref to an object and access a field typed ref, you see the field as ref. If you hold a box to an object and access a field typed ref, viewpoint adaptation gives you a box view of the field — you can read but not write, even though the field’s declared capability is ref. If you hold an iso to an object and access a field typed ref, you see the field as iso (unique through the unique reference). Viewpoint adaptation is the source of many counterintuitive compiler errors: a field declared ref accessed through a val receiver appears as val, preventing mutation even if you expected ref access.
consume is the mechanism for capability transitions and ownership transfer. consume expr moves the value of expr to a new binding and renders the original binding unusable for the rest of its scope (the Pony compiler tracks this via linear typing for iso references). If x: Data iso exists in scope, consume x produces a new Data iso value and x can no longer be used. The consumed value can be sent to an actor behaviour as a message (since iso is sendable), assigned to a new variable, or passed to a method that expects an iso argument. recover blocks create a new capability context where only val and tag references from outer scope are accessible, allowing the creation of an iso or val object that captures only safe references: recover iso Array[U8].create() end creates an Array[U8] iso by ensuring no external aliases exist during construction. recover ref ... end creates a ref from a recover block where only sendable references from outer scope were used. The standard pattern for constructing a complex iso object is to use recover iso [construction code] end to build the object mutably during construction and emerge from the block with a uniquely-owned iso reference.
Pony’s type system uses structural subtyping for interfaces. An interface in Pony is a set of method signatures; a class implicitly provides the interface if it has all the required methods with compatible signatures and capabilities. interface Updatable be update(data: Data iso) declares an interface with a behaviour; any actor with a be update(data: Data iso) behaviour satisfies this interface. Traits (nominally typed interfaces) use trait instead of interface and require explicit is Trait declarations on classes. Union types: (A | B | C) is the type of a value that may be A, B, or C; pattern matching on unions uses match value with | TypeA as var => ... arms. Intersection types: (A & B) is the type of a value that satisfies both A and B simultaneously. Generics: class Container[T: Equatable[T] #read] declares a generic class parameterized by T, constrained to types that are Equatable with a #read capability constraint (T references are at most readable). Capability constraints on type parameters: #read means the type parameter can be any capability that is at least read (box, ref, val, iso, trn); #send means the type parameter must be sendable (iso, val, tag, or primitives); #share means the type parameter must be shareable (val, tag, or primitives).
Pony’s actor model: behaviours, message passing, ORCA GC, and error handling
Pony actors are the unit of concurrency. An actor is defined with the actor keyword; it has fields (which are private to the actor’s own execution), be behaviour declarations (asynchronous message handlers called by other actors), and regular fun method declarations (synchronous, callable only from within the actor or from outside via a tag reference that calls only tag-safe methods). A behaviour declaration: be receive(data: Payload val) => _process(data). When another actor calls worker.receive(payload)), the message is placed in worker’s message queue; the Pony runtime’s work-stealing scheduler eventually runs the behaviour on a thread assigned to worker. All field accesses in a behaviour run without locks because the Pony scheduler guarantees that only one behaviour of a given actor executes at a time (actors are singly-threaded execution contexts).
Message payloads must consist of sendable values: iso (transferred via consume, giving the receiver unique ownership), val (immutable, safely shared), tag (actor references, safely shared), primitives (U8, U32, F64, etc., which are always val), and tuples or arrays of the above. A ref-typed object cannot be sent as a message: doing so would create an alias in the receiving actor’s context while the sending actor still holds the original reference, violating reference uniqueness. The compiler enforces this: attempting to pass a ref-typed value to a behaviour parameter typed iso is a compile-time capability error. The fix is to either change the parameter type to accept a ref value (possible only if the parameter will be used locally within the actor), change the object to val (if immutability is acceptable), or use consume to transfer an iso-typed value and have the sending actor relinquish ownership. Promise[T] is the standard mechanism for request-response patterns between actors: the requesting actor creates a Promise[T], sends a message with the promise to the fulfilling actor, and registers a then callback that receives the result. The fulfilling actor calls promise.resolve(result) or promise.reject(error). Promises (plural) provides combinators: Promises[T].join(promises) waits for all promises in a collection to resolve.
The ORCA (Object Reference Counting with Actors) protocol is Pony’s garbage collection mechanism. Unlike stop-the-world GC, ORCA runs concurrently with application code: each actor has its own heap, and objects are collected when the actor that owns them detects that no references to them exist in any other actor’s heap or message queue. ORCA uses a distributed reference counting scheme where actors track outgoing references (references that have been sent to other actors via messages) and incoming references (references received in messages). When an actor sends a reference to another actor in a message, it increments a counter for that reference; when the receiving actor processes the message and eventually discards the reference, it decrements the counter in a subsequent message back to the owning actor. The owning actor can collect the object only when its reference count from all actors reaches zero. The consequence for retainer engineering: reference cycles between actors cannot be collected by ORCA. An actor A holding a reference to actor B in its fields, and actor B holding a reference to actor A, creates a cycle that ORCA cannot detect as garbage. The design solution is to use tag capabilities for back-references (which provide only identity, not read/write access, and break the reference-ownership cycle for ORCA’s purposes), or to use explicit lifecycle management where one actor notifies the other before dropping its reference.
Pony’s error handling uses partial functions and the ? operator instead of exceptions or option types. A function or behaviour that can fail is declared with ? after its return type: fun parse_int(s: String): U64 ? => s.read_int[U64]()._1. Within a partial function, calling another partial function propagates the error with ?: let n = parse_int(input)?. A try block catches errors: try parse_int(input)? else default_value end returns default_value if parse_int fails. try ... then ... else ... end adds a then clause that runs on success and an else clause that runs on failure; the then clause can itself contain partial operations (with ?). Pony has no checked exception hierarchy or exception types: the error signal carries no information beyond the fact that the function failed; error context must be communicated through return values or actor messages before the error is signaled. The None type (Pony’s unit type) is often used as an explicit “not found” return value in union types like (T | None), which is the Option[T] equivalent; but ? propagation is preferred for functions where failure is the exception rather than the common case. The Pony standard library uses Array[T].apply(i: USize): this->T ? (indexed access that errors if out of bounds) as the canonical example of partial function use.
How HourTab tracks Pony developer retainer hours
Pony retainer work shares the invisible-work problem with all systems programming retainers, with the additional challenge that Pony’s most common retainer tasks — reference capability annotation, sendable type audit, actor communication protocol restructuring, ORCA cycle analysis — produce diffs whose surface area is small relative to the analytical work required. Changing a class type annotation from ref to iso and adding consume at five call sites is a diff with six lines changed; the value is compile-time-verified absence of data races for the lifetime of the engagement, with no mutex, no lock, and no race detector. Restructuring four actor pairs from shared ref-typed state to Promise[T] request-response is a diff that rewrites the message protocol; the value is zero race conditions in a system that previously had four per session. Adding #send capability constraints to four generic type parameters is a diff with four modified type declarations; the value is a generic class that can safely be used in actor message payloads without compile-time errors.
HourTab gives Pony developers a public retainer-hours URL they send to clients — typically infrastructure teams building high-performance concurrent systems, trading systems requiring lock-free data processing, or systems programming teams migrating from C++ actor libraries to a language with compile-time race freedom — at the start of an engagement. For Pony retainers, each work log entry should name the mechanism (reference capability annotation audit iso/val/ref/box/tag/trn; viewpoint adaptation analysis for field access; consume/recover block design for capability transitions; sendable type audit for behaviour parameters; Promise[T] pipeline design; IFulfill[T]/IReject callback behaviour authorship; TCPNotify actor hierarchy design; ORCA cross-actor reference cycle analysis; iso ownership transfer protocol design; Finalizable trait implementation; ? partial function propagation; try/then/else error recovery; C-FFI @pony_ffi declaration), the specific class or actor name and the capability problem, and the before/after observable metric. Pony retainers are often compared to Erlang developer retainers for actor-model concurrency work, to Rust developer retainers for ownership-typed systems programming, and to Haskell developer retainers for type-driven correctness guarantees. The distinction from all three is compile-time data race freedom: Pony’s reference capability system guarantees at the type level that no two actors can have mutable access to the same object simultaneously, a guarantee that Erlang’s message-passing model approaches but does not encode in the type system, that Rust’s ownership system provides within a thread but not across actor/thread boundaries, and that Haskell’s type system does not address for concurrent mutation. HourTab’s work log makes that distinction legible to clients.
Track Pony developer retainer hours without the status emails
HourTab gives Pony 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 work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Pony developer retainers
What does a Pony developer on retainer typically do?
A Pony developer on monthly retainer covers four principal service areas: reference capability design (iso/val/ref/box/tag/trn annotation audit; capability subtype verification against the Pony capability subtype lattice; viewpoint adaptation analysis for field access through varying receiver capabilities; consume and recover block design for capability transitions; structural subtype interface capability alignment); actor behaviour design (be behaviour declaration design; sendable type audit for behaviour parameters; Promise[T] pipeline design for request-response; IFulfill[T]/IReject callback behaviour design; notify pattern design using interface traits; TCPListener/TCPConnection/TCPNotify actor hierarchy design); ORCA GC integration (cross-actor reference cycle analysis; iso ownership transfer design with consume; Finalizable trait implementation; message queue backpressure monitoring); and error handling design (? partial function propagation; try/then/else error recovery; union type (T | None) vs ? convention design).
What Pony work is most commonly underlogged in a retainer?
Reference capability redesign (shared mutable aggregation object typed ref allowing aliased box write paths from callback behaviours; changed to iso with consume on each transfer; corruptions: 5/day → 0; 16–24 hrs invisible in capability subtype analysis and API restructuring), sendable type audit for behaviour parameters (12 behaviour parameters typed ref or box causing compiler errors after refactoring to add a second actor receiving the same messages; restructured payloads to use val for immutable shared objects and iso/consume for mutable transferred objects; compiler errors: 12 → 0; 10–18 hrs invisible in type annotation revision), and Promise[T] request-response pipeline design (4 actor pairs using mutable shared state for synchronous result communication; replaced with Promise[T] for asynchronous response delivery; race conditions: 4/session → 0; 14–22 hrs invisible in async pipeline restructuring). Each produces a compile-time-verified race-free system where the previous version had data races invisible to the compiler due to incorrect capability annotation.
What are typical Pony developer retainer rates?
Entry-level Pony developers (1–2 years, basic reference capabilities iso/val/ref/box/tag, simple actor behaviour design, consume/recover, basic try/then/else, foundational standard library Array/String/Map/Set) bill at $75–$130/hr. Mid-level Pony engineers (2–4 years, capability subtype analysis for complex class hierarchies, sendable type audits, Promise[T] and IFulfill[T]/IReject pipeline design, TCPListener/TCPConnection/TCPNotify actor systems, ORCA cross-actor reference cycle analysis, structural subtype interface design) bill at $120–$220/hr. Senior Pony architects (4–8 years, full actor system architecture with work-stealing scheduler tuning, C-FFI @pony_ffi declarations, complex generic type parameter design with capability constraints, large-scale iso ownership transfer protocols, deep ORCA protocol memory behavior analysis) bill at $180–$335/hr. Monthly retainer ranges: $2,500–$5,800/mo advisory (15–25 hrs), $9,000–$23,000/mo for full actor systems platform engagements.
What should a Pony developer retainer agreement include?
A Pony developer retainer agreement should specify: reference capability scope (iso/val/ref/box/tag/trn annotation audit; capability subtype verification; viewpoint adaptation analysis; consume/recover block design; alias analysis for compiler-rejected operations; structural subtype interface capability alignment); actor behaviour scope (be behaviour declaration design; sendable type audit; Promise[T] pipeline design; IFulfill[T]/IReject callback design; TCPListener/TCPConnection/TCPNotify actor hierarchy; behaviour queue backpressure analysis); ORCA GC scope (cross-actor reference cycle analysis; iso ownership transfer design; Finalizable trait implementation; message queue backpressure monitoring); error handling scope (? partial function propagation; try/then/else error recovery; error union type definition; None vs ? convention migration); type system scope (structural subtype interface definition; generic class [A: Type] design; union type (A | B | C); intersection type (A & B); primitive val capability properties); and hour logging format (class/actor name; capability annotation changed; corruption count or compiler error count before/after; Pony compiler version).
How should Pony developer retainer hours be logged?
Log each Pony retainer session with: advisory category (reference capability annotation audit; viewpoint adaptation analysis; consume/recover block design; sendable type audit for behaviour parameters; Promise[T] request-response pipeline design; IFulfill[T]/IReject callback behaviour authorship; TCPNotify actor hierarchy design; ORCA cross-actor reference cycle analysis; iso ownership transfer protocol design; Finalizable trait implementation; ? partial function propagation; try/then/else error recovery; C-FFI @pony_ffi declaration; structural subtype interface definition), the specific class or actor name and the capability problem (shared mutable aggregation object typed ref allowing aliased box write paths; changed to iso with consume on each transfer; corruptions: 5/day → 0), and the before/after metric (data corruptions/day: 5 → 0; compiler capability errors: 12 → 0; race conditions/session: 4 → 0). Include ponyc version, target platform, and whether the fix required capability annotation changes, consume/recover restructuring, or actor communication protocol redesign.