tidwall/neco: single-threaded coroutines for C I/O
Concurrency library for C (coroutines)
At a glance
- What is it?
- Neco is a dependency-free C library for stackful coroutines, channels, deadlines, cancellation, and POSIX-oriented I/O on one thread. It fits programs modeled like pthreads or Go, but it explicitly leaves multi-core scheduling and async/await models out of scope.
- Who is it for?
- Adopt Neco when a C program needs cooperative concurrent I/O without a multi-threaded scheduler, and avoid it when work must scale across CPU cores or when the required async/await model is mandatory. Verify first that the documented build command completes with your compiler, then check the platform notes and argument lifetime rules before moving channel or file-descriptor code into production.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 146 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Star history
- tidwall/neco1,383 ★ · +1
| Repository | 2026-10-16 | 2026-10-23 |
|---|---|---|
| tidwall/neco | 1.4k | 1.4k |
1 Stars in the last 7 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Neco's place in single-threaded C concurrency
Neco targets C programs that need several tasks to wait for I/O, timers, signals, or one another without blocking the progress of unrelated tasks on the same thread. The README describes it as a library for concurrent I/O and network programming, with an API intended to resemble the usability of pthreads or Go while remaining within C. This is cooperative, single-threaded concurrency, not parallel execution across CPU cores.
That boundary is explicit. A non-goal is a scalable multithreaded runtime in which several CPU cores share the coroutine scheduler. Neco also states that async/await is not its concurrency model. It is therefore a poor match for a program whose central requirement is CPU-bound work distribution or a language design based on asynchronous functions. For an event-driven network service built around file descriptors, however, its scheduling and synchronization APIs address a concrete problem.
The library targets systems with different levels of support. Linux, macOS, and FreeBSD are named as supported platforms. WebAssembly and Windows are also mentioned, but the README links to platform notes that describe limitations. Those limitations matter for adoption because a portable project name does not mean that every platform has the same networking, assembly, or scheduler capabilities.
Inside Neco's coroutine scheduler
Neco uses stackful coroutines managed by a lightweight runtime and scheduler for the current thread. The README says context switching happens in user space and uses assembly in most cases. Coroutines can be nested, and the scheduler manages their lifetimes. The stated scheduler is fair and deterministic, which makes ordering easier to reason about than an uncontrolled race between OS threads, although the README does not publish timing figures or ordering guarantees for every API.
Execution is organized around operations such as starting, sleeping, suspending, resuming, yielding, and joining coroutines. When `neco_start()` performs the first runtime initialization, it initializes Neco for the calling thread and blocks until the started coroutine and its child coroutines terminate. This entry behavior means the first call can establish the scheduler and run the program rather than behaving like an ordinary function that immediately returns to its caller.
Synchronization is cooperative as well. Neco provides channels, generators, mutexes, condition variables, and waitgroups. Channels move messages between coroutines, while the other primitives coordinate coroutine execution within the same runtime. The exact fairness and blocking semantics of each primitive are delegated to `docs/API.md`; the README lists the facilities but does not reproduce their full contracts. That distinction is important for code that depends on edge cases such as channel closure, cancellation races, or mutex ownership during coroutine switches.
Adding neco.c and neco.h to a C project
Neco has no documented dependency installation step. The repository provides a single-file amalgamation, and the README instructs users to add `neco.c` and `neco.h` to the project. Its stated build check is to compile the implementation file with a modern C compiler. Put the repository's `neco.c` and `neco.h` where the compiler can include the header and compile the source.
The documented command is:
cc -c neco.cThis compiles `neco.c` into an object file. The README does not give a complete application build or linker command, so it would add an unsupported assumption to combine this operation with a particular output name or executable template.
A first program can use `neco_main()` in place of the standard `main()` when the entire program is intended to run from coroutines:
#include <stdio.h>
#include "neco.h"
int neco_main(int argc, char *argv[]) {
printf("main coroutine started\n");
return 0;
}According to the example, a successful execution prints `main coroutine started` and then returns from `neco_main()`. The README links to the API reference for changes in program behavior introduced by `neco_main()`, so this substitution should be checked against the full build and error-handling contract rather than treated as an undocumented alias.
Running timers and concurrent coroutines
The simplest useful Neco pattern starts independent coroutines and keeps the program alive while they operate. The following example comes from the README. One coroutine sleeps for one second before printing `tick`; the other sleeps for two seconds before printing `tock`. The main coroutine remains alive for one hour.
#include <stdio.h>
#include "neco.h"
void ticker(int argc, void *argv[]) {
while (1) {
neco_sleep(NECO_SECOND);
printf("tick\n");
}
}
void tocker(int argc, void *argv[]) {
while (1) {
neco_sleep(NECO_SECOND*2);
printf("tock\n");
}
}
int neco_main(int argc, char *argv[]) {
neco_start(ticker, 0);
neco_start(tocker, 0);
neco_sleep(NECO_HOUR);
return 0;
}This code demonstrates why sleeping and scheduler lifetime are connected. The first `neco_start()` call can block until its coroutine and children terminate, but these two loops never terminate. `neco_sleep(NECO_HOUR)` keeps the main coroutine active while the scheduler services the timers. An application that returns after starting such loops may leave no active coroutine to keep the runtime alive.
The example also exposes a design constraint: coroutine arguments are represented by an integer count and an array of `void *` values. Neco does not copy arbitrary pointed-to objects for the caller. They must remain valid until the coroutine's first `neco_` call, or the program must copy and validate them before that suspension point. After the first such call, the README says using the argument array is unsafe.
Channel ownership in Neco's coroutine API
Channels provide message passing between coroutines, but ownership must be managed explicitly. The README recommends retaining a channel before passing it to a new coroutine, then releasing the coroutine's reference after use. This prevents a channel from being freed while another coroutine still refers to it.
The following example creates an unbuffered channel for `char*` values, retains it before starting the sender, receives the string, and prints it:
#include <stdlib.h>
#include "neco.h"
void coroutine(int argc, void *argv[]) {
neco_chan *messages = argv[0];
char *msg = "ping";
neco_chan_send(messages, &msg);
neco_chan_release(messages);
}
int neco_main(int argc, char *argv[]) {
neco_chan *messages;
neco_chan_make(&messages, sizeof(char*), 0);
neco_chan_retain(messages);
neco_start(coroutine, 1, messages);
char *msg = NULL;
neco_chan_recv(messages, &msg);
printf("%s\n", msg);
neco_chan_release(messages);
return 0;
}The printed value is `ping`. The final zero passed to `neco_chan_make()` denotes an unbuffered channel according to the README's description. This is not garbage collection. Neco exposes explicit retain and release operations, so every new coroutine reference needs a matching ownership decision. Pointer validity also extends beyond the channel object: the example sends a pointer to a string literal and reads it before release, but the README does not claim that arbitrary heap buffers are copied by the channel.
For a fuller application, the repository includes `examples/channels.c`, `examples/channel-buffering.c`, and `examples/generators.c`. Those files provide more concrete cases than the abbreviated README channel sample.
POSIX-oriented networking and time-based cancellation
Neco is designed to sit close to POSIX-style C rather than require a separate network abstraction. Its features include a POSIX-friendly interface using file descriptors, networking utilities, signals, streams, buffered I/O, random data, deadlines, and cancellation. The repository also contains `examples/echo-server.c`, `examples/echo-client.c`, `examples/select.c`, and `examples/signals.c`, confirming that networking and signal flows are represented beyond the core coroutine examples.
The README does not show the signatures or semantics for those networking and deadline APIs, so it would be inaccurate to promise a particular readiness API, timeout unit, cancellation cause, or file-descriptor registration procedure. The README directs readers to `docs/API.md` for the details. That limitation affects evaluation: adopting Neco for a network service requires checking whether its deadline and cancellation contracts cover the exact failure cases expected by the service, not merely whether example files exist.
Neco's single-threaded scheduler also changes the performance and contention question. Blocking operations that suspend a coroutine can allow other coroutines on the same thread to proceed, but CPU-heavy loops do not become parallel merely because they run inside separate coroutines. The README's explicit refusal to provide a scheduler shared among multiple CPU cores is the clearest guidance on that boundary.
Maintenance, platform limits, and upgrade work in Neco
The repository is not archived, and its last push was on 2026-05-30. No GitHub releases are listed. That means the project cannot be upgraded through a documented release sequence visible in the repository, even though recent repository activity exists. Users should treat the repository contents as the version being considered. There is no release tag or migration note in the repository from which to infer upgrade compatibility.
Neco's amalgamated layout reduces dependency management. `neco.c` and `neco.h` can be copied into a project, and the README lists a test suite with 100% coverage using sanitizers and Valgrind. Those maintenance signals concern the repository's own testing claims, not a guarantee that every integration is free of platform or ownership defects. No automated compatibility matrix, supported compiler matrix, or release cadence is documented in the README.
The MIT license permits use, modification, distribution, and sublicensing under its terms, subject to the license text. Consumers still need to retain the required copyright and license notices when redistributing copies or substantial portions. The main upgrade cost is likely source review and integration testing rather than dependency reconciliation, but that conclusion follows from the two-file layout and absence of listed dependencies, not from a published upgrade policy.
Editorial conclusion
Adopt Neco when a C program needs cooperative concurrent I/O without a multi-threaded scheduler, and avoid it when work must scale across CPU cores or when the required async/await model is mandatory. Verify first that the documented build command completes with your compiler, then check the platform notes and argument lifetime rules before moving channel or file-descriptor code into production.
Frequently asked questions
What is tidwall/neco used for?
Neco provides stackful coroutines, synchronization primitives, deadlines, cancellation, and POSIX-oriented I/O for concurrent C programs. Its scheduler is designed for a single thread rather than work shared across multiple CPU cores.
How do I install tidwall/neco?
The repository has no dependency installation instructions. Add `neco.c` and `neco.h` to the C project, then use the documented `cc -c neco.c` compilation step.
Does tidwall/neco support multithreading across CPU cores?
No. A scalable multithreaded runtime with a scheduler shared among multiple CPU cores is an explicit non-goal. Neco is a fast single-threaded concurrency library for C.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tidwall-neco)Star history chart for your README
[](https://hysenlabs.com/projects/tidwall-neco)Was this useful?