100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

core.async Basics

core.async brings Go-style CSP concurrency to Clojure through channels and lightweight go blocks, decoupling producers and consumers.

State & ConcurrencyIntermediate10 min readJul 10, 2026
Analogies

Why core.async? CSP-Style Concurrency

Atoms, refs, and agents all manage state that's read and written directly by cooperating code. core.async takes a different approach, modeled on Communicating Sequential Processes (CSP) — the same model behind Go's goroutines and channels. Instead of sharing mutable state, independent 'processes' communicate purely by passing messages through channels, which decouples producers from consumers: neither side needs to know how fast the other is running or even that the other exists, as long as both agree on the channel between them.

🏏

Cricket analogy: Like a bowling machine, the producer, feeding balls into a chute and a batter, the consumer, taking them at their own pace — core.async's channels decouple producer and consumer so neither needs to know the other's timing, similar to how net practice sessions run independently of match scheduling.

core.async ships as a separate library (org.clojure/core.async), not part of clojure.core, and must be required explicitly. Its go blocks compile your code into a state machine so that 'parking' operations (>! and <!) can suspend without blocking an actual OS thread.

Channels: chan, Buffers, and Closing

You create a channel with (chan) for an unbuffered channel, or (chan n) for one with a fixed buffer capacity of n. Blocking put and take — >!! and <!! — are used on real threads, while their parking counterparts >! and <! are used inside go blocks. Once a buffered channel's capacity is full, further puts block or park until a consumer frees space by taking an item, giving you built-in backpressure; calling close! on a channel lets any buffered values still drain normally, after which every further take immediately returns nil instead of waiting.

🏏

Cricket analogy: Like a ball-return chute at a bowling machine with a fixed capacity of 6 balls, a buffered channel — the machine (>!! caller) blocks once the chute is full until the batter (<!! caller) removes one; closing the chute (close!) signals no more balls are coming.

clojure
(require '[clojure.core.async :as async :refer [chan go >! <! >!! <!! close! alts! timeout]])

(def orders (chan 10)) ;; buffered channel, capacity 10

(go
  (loop []
    (when-let [order (<! orders)]
      (println "processing order" order)
      (recur))))

(go (>! orders {:id 1 :item "keyboard"}))
(go (>! orders {:id 2 :item "mouse"}))

(close! orders)

;; racing two channels with a timeout
(let [fast (chan) slow (chan)]
  (go (<! (timeout 100)) (>! fast :fast-result))
  (go (<! (timeout 500)) (>! slow :slow-result))
  (go
    (let [[val ch] (alts! [fast slow (timeout 1000)])]
      (println "got" val "from" (if (= ch fast) "fast" "slow-or-timeout")))))

go Blocks and Parking Operations

The go macro takes a body of code and compiles it into a state machine that runs as a lightweight process on a small, fixed thread pool, rather than a dedicated OS thread. Inside a go block you should always use the parking operations >! and <! instead of the blocking >!! and <!!, because parking suspends the state machine and frees the underlying pool thread for other go blocks, whereas a genuinely blocking call — including calling a blocking function like Thread/sleep — ties up that thread and can starve every other go block waiting to run on the same pool.

🏏

Cricket analogy: Like a net-practice bowler who politely steps aside and lets another bowler use the net while waiting for a ball to be returned, a parking >!/<! inside a go block, freeing the underlying thread, rather than standing there refusing to let anyone else use the net, a blocking >!!/<!! call that would hog a whole thread.

Never call a blocking operation like <!! or a long Thread/sleep inside a go block — go blocks run on a small fixed thread pool, and a single blocking call can starve every other go block scheduled on that pool. Use <! and >! (parking) inside go blocks, and reserve <!!/>!! for real threads.

Selecting with alts! and Timeouts

Often you need to react to whichever of several channel operations becomes ready first, rather than waiting on one specific channel — that's exactly what alts! (inside a go block) and its blocking counterpart alts!! provide: you pass a vector of channels or put-operations, and it returns a [value, channel] pair for whichever one completed first. Combined with the timeout function, which returns a channel that automatically closes after a given number of milliseconds, alts! is the standard way to implement a deadline or 'give up waiting' pattern in core.async pipelines.

🏏

Cricket analogy: Like a captain choosing whichever bowler becomes available first among two warming up in the nets (alts! selecting whichever channel is ready first), with a rain-delay clock (timeout channel) forcing a decision if neither is ready within 30 minutes.

  • core.async channels implement CSP-style message passing between independent processes.
  • Buffered channels apply backpressure: puts park/block once the buffer is full.
  • go blocks run lightweight processes on a small thread pool — use parking >!/<! inside them, never blocking calls.
  • close! signals completion; takes on a closed, drained channel return nil.
  • alts!/alts!! select whichever of several channel operations completes first.
  • timeout channels combined with alts! implement deadlines and give-up-waiting patterns.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ClojureStudyNotes#CoreAsyncBasics#Core#Async#CSP#Style#Concurrency#StudyNotes#SkillVeris