Inside cron-job.org: the chronos daemon, a Thrift node protocol and per-day SQLite logs
cron-job.org Open Source project
At a glance
- What is it?
- cron-job.org is a self-hosted scheduled HTTP request system whose execution daemon, chronos, spawns one thread per minute and keeps results in per-user per-day SQLite files. Adopting it means compiling curl, c-ares, thrift and the daemon yourself, and the shipped container setup is labelled a demo rather than a production deployment.
- Who is it for?
- cron-job.org earns its C++ and PHP stack when many people need scheduled HTTP calls managed from a browser, with per-user logs and optional fan-out across execution nodes, and it is excess weight when one administrator has twenty URLs a system crontab and curl already handle.
- Can I use it commercially?
- Yes, with conditions. GPL-2.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly JavaScript, 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.
Editorial analysis
What cron-job.org schedules, and for whom
cron-job.org is a self-hosted system for repeated HTTP requests on a schedule. The repository ships six working parts: a MySQL schema in `database`, the `chronos` execution daemon, interface definitions in `protocol` for interaction between system nodes, a React web console in `frontend`, a status page in `statuspage`, and a PHP API in `api` that both user interfaces call. The people it fits are operators who want to call a URL on a timer and then read what came back, managed from a browser instead of by editing crontabs on a box.
The unit of work is an HTTP request. The chronos section names the curl multi library as the component that performs the actual fetching, and nothing in the repository describes running a shell command or parsing crontab syntax on a target host. If the job you need is a database dump or a nightly backup script, this is the wrong project, and no amount of configuration changes that.
Execution is also split by node. chronos can run on multiple nodes, but each node requires its own MySQL server and database and stores its own jobs, while the host running the web interface manages the user database plus the association between a job and a node. Creating, deleting, updating and fetching jobs and job logs all travel over a Thrift-based protocol defined in the `protocol` folder.
One thread per minute across libev and curl multi
chronos checks the MySQL database every minute to collect all jobs that are due. For every minute a thread is spawned, and that thread processes every job in the slot. Concurrency follows a clock grid rather than a per-job pool: a job due at 03:14 cannot begin before the 03:14 thread exists, and two jobs falling in the same minute share one event loop. A scheduler that can only express per-minute resolution will surprise anyone expecting second-level cron syntax.
The event loop is libev, the HTTP layer is curl multi, and resolution goes through c-ares, which the chronos section says together allow for thousands of parallel HTTP requests. Build prerequisites make that intent concrete: curl, preferably with c-ares as resolver and with libidn2 for IDN support, plus libev, mysqlclient, sqlite3, and the thrift compiler with libthrift. A C++14 compiler and cmake sit on top. Thousands of concurrent sockets inside one thread is the design point, not thousands of threads.
Because the minute grid is shared, the daemon and the database have to agree on the clock. The README does not document clock skew handling between an execution node and the web interface host, so a node whose time drifts would collect a different set of due jobs on each pass.
Per-user, per-day SQLite files hold the job history
Job definitions live in MySQL. Results do not. chronos writes each user's output into per-user, per-day SQLite databases, because the project states that storing result data in MySQL can quickly lead to I/O bottlenecks and that cleaning up old MySQL entries is extremely expensive. Cleanup becomes a file operation instead of a delete query: remove the databases for the days you no longer want. Retention is therefore decided by which day folders you keep on disk.
The same decision sets the durability ceiling. The chronos notes say the software is optimized on performance rather than on data integrity, so a server crash, a power outage or a hardware defect takes the recent job history with it. Log retention here is a convenience for reading the last few days of output, not an audit trail.
Two database technologies therefore sit inside the same daemon, and the compose file shows why both are needed: MySQL 8.3 containers initialise the master and node schemas, while sqlite3 remains a build prerequisite for chronos itself.
Compiling chronos with cmake and five system libraries
The daemon is a C++ project built with cmake. Create a build folder, configure the project from inside it, then compile:
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
makeBefore the binary runs you import the DB scheme from the `database` folder, customise `chronos.cfg` for your system with your MySQL login filled in, and pass the config path on the command line:
./chronos /path/to/chronos.cfgThe shipped container build shows how much of that stack is compiled rather than packaged. On ubuntu:noble it installs build tooling, then downloads and builds cmake 3.28.3, c-ares 1.34.5, curl 8.4.0 with `--enable-ares=/opt/chronos`, and thrift 0.13.0, before running the same cmake configure step with an install prefix under `/opt/chronos`. Even in the example environment, chronos talks to a curl the image compiled itself, not to a distribution package.
The API, the console, and eight required secrets
The API folder is PHP and has to be hosted. cron-job.org itself serves it with nginx and php-fpm, and the documented requirement is PHP 7; a redis instance is optional and exists to support API call rate limiting. Going live means copying the `api/` folder to the webserver, then saving `config/config.inc.default.php` as `lib/config.inc.php` and editing it for the environment.
Both React front ends follow the same four steps in their own directories. `frontend` and `statuspage` each need Node.js, an `npm install`, a copy of `src/utils/Config.default.js` saved as `src/utils/Config.js`, and `npm start` to serve the bundle.
The containerised version moves the same settings into a `.env` file, and that is where the strictness sits. Eight variables ending in `_SECRET` are expected to hold unique random values of about 32 characters, and the file states plainly that the API refuses to operate while they are empty or unset. Among them are `CJO_SESSION_TOKEN_SECRET`, `CJO_EMAIL_VERIFICATION_TOKEN_SECRET`, `CJO_VERP_SECRET` and `CJO_STATUS_BADGE_TOKEN_SECRET`. Outgoing mail then needs `SMTP_SERVER`, and the example uses no SMTP authentication at all, so the mail server has to allow relaying for your machine's IP address.
Thrift builds without PHP, yet the API glue needs it
The `protocol` folder holds the Thrift interface definitions used between nodes, and the API notes warn that changing the protocol means re-compiling the PHP glue code and copying it into `lib/protocol/`, a step the project calls manual and expects to be committed.
The container build cannot produce that glue. The thrift 0.13.0 configure line in the Dockerfile disables every language binding except C++:
./configure --prefix=/opt/chronos --with-php=no --with-erlang=no --with-go=no --with-java=no --with-python=no --with-py3=no --with-ruby=no --with-nodejs=no --with-c_glib=no --with-cpp=yesSo the image builds chronos and the C++ side of the protocol, and leaves the PHP side to a compiler you install elsewhere. Anyone who edits an interface definition has to provide a thrift compiler with PHP support outside the container, or the console and the daemon quietly disagree about the same call. Nothing in the repository automates that step, and the generated PHP is meant to be committed by hand.
A single-node compose file that warns against production use
The fastest route is the example environment. Initialise the submodules, copy the env template, fill the secrets, then bring the stack up and open the frontend on port 8010:
git submodule init && git submodule update
docker compose upTwo comments in `docker-compose.yml` set expectations. It calls itself a demo configuration for a system running with a single execution node, and states that it is not hardened or tuned for security and must not be used in production. The README repeats the point: the Docker environment in the repo is an example and development environment, not tailored for production use.
Reading that compose file closely turns up details the instructions never mention. MySQL 8.3 starts with `--default-authentication-plugin=mysql_native_password`, `--sql_mode=""` and `--tls-version=""`, so SQL mode checking is switched off and legacy password authentication is chosen deliberately. The master database initialises four scripts, and its second one is mounted as `01_config_master.sql` from `docker-data/config/config_master.sql`, not from the tracked `database` folder. By contrast the node schema mount carries no numeric prefix at all, which leaves the init order to filename sorting inside the entrypoint directory. The shipped defaults for the master MySQL root password, user, password and database are all `chronos`.
Development mode takes a second compose file and fails in documented ways. Running `docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build` serves both front ends through the React dev server with source live-mounted, and the first start answers `Bad Gateway` until the logs report `Compiled successfully!`. Seeing `npm: command not found` in the `frontend` or `statuspage` logs means the containers are still on stale production images, because dev mode builds separate `cron-joborg-frontend-dev` and `cron-joborg-statuspage-dev` tags.
The last push landed on 2026-10-05 and the repository has no GitHub releases, so there is no tagged upgrade to track and you follow the `master` branch commit by commit.
What a plain system crontab already covers for free
The honest comparison is with the crontab already on the server. A system crontab plus curl can call a URL every five minutes with no database, no daemon, no API and no build step. cron-job.org charges for all four: a MySQL scheme to import, a C++ daemon to compile, a PHP API to host, and two React bundles to build.
What that buys is specific. Several users share one scheduler behind a login, a web console creates and edits jobs, and per-user job logs are read in a browser instead of hunted down in mailboxes. A status page exists as a separate UI for outages. A shared crontab gives none of that, and for one administrator with twenty URLs the added surface is not worth it.
Two cases argue against it outright: a job that must run a shell command rather than fetch a URL, and a requirement that job history survive a disk failure. The licence matters too for anyone running the service for other people, since the code ships under GPL-2.0 and a modified API or daemon exposed to users carries the same terms.
Editorial conclusion
cron-job.org earns its C++ and PHP stack when many people need scheduled HTTP calls managed from a browser, with per-user logs and optional fan-out across execution nodes, and it is excess weight when one administrator has twenty URLs a system crontab and curl already handle. Verify three things first: that chronos.cfg points at a MySQL loaded from the database folder, that all eight _SECRET values in .env are set, and that losing the per-day SQLite job history after a crash is acceptable for your use.
Frequently asked questions
What does cron-job.org do, and who is it built for?
It is a self-hosted system for scheduling HTTP requests: a MySQL schema holds job definitions, the chronos daemon executes them one minute at a time, and a PHP API serves a React web console and a status page. It fits operators who want a browser interface and per-user logs rather than server-side crontabs.
Can cron-job.org spread scheduled jobs across several servers?
Yes, chronos can run on multiple nodes, and each node requires its own MySQL server and database and stores its own jobs. The host running the web interface keeps the user database and the association between a job and a node, and talks to the nodes over a Thrift-based protocol defined in the protocol folder.
How do I try cron-job.org without compiling chronos myself?
Use the example container environment: initialise the git submodules, copy the env template, fill the required secrets, run docker compose up, and open the frontend on port 8010. The project describes that environment as a demo for a single execution node, not as a production setup.
What happens to cron-job.org job results if the server crashes?
Results go into per-user, per-day SQLite databases rather than MySQL, and the project states that it optimizes for performance instead of data integrity. A crash, a power outage or a hardware defect therefore takes the stored job history with it.
What is needed to build chronos from source?
Development files for curl, preferably with c-ares as resolver and libidn2 for IDN support, plus libev, mysqlclient, sqlite3 and the thrift compiler with libthrift. A C++14 compiler and cmake are required, then you create a build folder, run cmake there and make.
Is the cron-job.org Docker environment safe for production use?
No. The compose file describes itself as a demo configuration that is not hardened or tuned for security, and the README states the Docker environment is intended as an example and development environment rather than for production.
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/pschlan-cron-job-org)Was this useful?