# go-judge runs untrusted programs in a container and tells you how they failed

> criyle/go-judge is an MIT licensed Go service that executes code in a restricted container behind a REST or gRPC API, and it reports a judged status such as time limit exceeded, memory limit exceeded, dangerous syscall or file error rather than a bare exit code. It is built for online judges, it needs privileges and cgroups to do its job, and its Windows and macOS backends are documented as experimental.

**criyle/go-judge** — Sandbox service built on Linux container technologies with simple REST and gRPC API

- Repository: https://github.com/criyle/go-judge
- Website: https://docs.goj.ac
- Stars: 631 · Forks: 104
- Language: Go
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/criyle-go-judge

## The status list is the actual product

go-judge is a sandbox service built on Linux container technologies with a simple REST and gRPC API, and the quickest way to understand it is to look at what it reports rather than at what it runs. The return status list distinguishes Accepted, where the program exited with status code 0 within the time and memory limits, from Memory Limit Exceeded, Time Limit Exceeded, Output Limit Exceeded, File Error, Non Zero Exit Status, Signalled, Dangerous Syscall and Internal Error. Each of those carries a reason in the documentation: a timeout usually shows exitStatus 9 because the program was killed by SIGKILL, a file error means the copy-in file did not exist or was too large for the container filesystem, a dangerous syscall means the program was denied by the default seccomp filter, and an internal error covers the case where the container could not be created, for example because the Docker daemon was not privileged. For an online judge that distinction is the whole job, and it is the part a generic container runtime does not give you.

## Three sandbox backends behind one transport layer

The architecture diagram in the README is a small picture of the whole design. A transport layer sits on top, described as HTTP, WebSocket and FFI among others, and below it is the sandbox worker, which is an environment pool with an environment builder. Under the worker sit the actual execution backends and the storage: EnvExec, which the diagram places alongside the Linux, Windows and macOS implementations, then a file store, shared memory and disk. The three platform backends are named in the diagram, Linux through go-sandbox, Windows through winc, and macOS through the app sandbox. The environment pool is the part that makes the service usable at volume, since building a sandbox is expensive and a pool lets the service reuse prepared environments rather than constructing one per request. The repository layout mirrors this, with worker/, envexec/, filestore/, seccomp/ and env/ directories at the top level alongside cmd/ and the generated protocol definitions in pb/.

## The REST surface is small enough to read in one sitting

The API is documented in the README as a short list. POST /run executes a program in the restricted environment, and the service listens on localhost:5050 by default. The file endpoints form their own small resource: GET /file lists all cached file id to original name map, POST /file prepares a file in the judge, in memory, and returns a fileId that can be referenced in a /run parameter, GET /file/:fileId downloads the file content, and DELETE /file/:fileId removes it. There are two WebSocket variants, /ws as a WebSocket version of /run and /stream for a streaming run interface. Two read-only endpoints round it off: GET /version returns the build git version, for example v1.9.0, together with runtime information such as the Go version, the operating system and the platform, and GET /config returns some configuration such as fileStorePath and runnerConfig together with the supported features. A gRPC endpoint exists as well and is disabled by default.

## Everything is configured with flags

The configuration section is a list of command line flags, and the defaults are conservative in a way worth noting. The default binding address for the go judge is localhost:5050, changeable with the -http-addr flag, and the gRPC endpoint is off until you add -enable-grpc, with its own default of localhost:5051 and a -grpc-addr flag. Logging is controlled by -silent or -release, and the release logger turns on automatically inside Docker. Security relevant flags are -auth-token, which adds token based authentication to REST and gRPC, and -enable-debug, which turns on the Go debug endpoints at localhost:5052/debug along with debug logging. Prometheus metrics at localhost:5052/metrics are off until -enable-metrics is passed, and the monitoring endpoint shares that port and can be moved with -monitor-addr. On the sandbox side, the default concurrency equals the number of CPUs and can be set with -parallelism, -mount-conf handles the file system mounts on Linux only, -file-timeout sets the maximum time to live for a file in the store, the file store lives in memory under /dev/shm unless -dir points at a local cache, -output-limit caps the POSIX rlimit on output at 256MiB by default, and -copy-out-limit caps the file copy out at 64MiB.

## The container root filesystem is a short list of bind mounts

On Linux the default mount points bind mount the host's /lib, /lib64, /usr, /bin, /etc/ld.so.cache, /etc/alternatives and /etc/fpc.cfg, along with the character devices /dev/null, /dev/urandom, /dev/random, /dev/zero and /dev/full, and they mount tmpfs at /w and /tmp and create /proc. The point of that list is that the container gets a working userspace without a full image, and that a program compiled on the host can run inside it. The working directory /w is where a judged program is expected to write, and tmpfs keeps it off the disk and out of the host filesystem. Two details are configurable. If mount.yaml is not specified, the size of the tmpfs for /w and /tmp comes from -tmp-fs-param with a default of size=128m,nr_inodes=4k, and the example mount.yaml file shows how to customise the mount points. There is also a small convenience, since a file named /.env in the container root causes the container to load it as environment variables line by line.

## Isolation needs cgroups, seccomp and privileges

The security model is the part to read carefully, because the README says the service must be privileged to do its job. For cgroup v1, go-judge needs root privilege to create the cgroup, either by creating the sub-directories /sys/fs/cgroup/cpuacct/gojudge, /sys/fs/cgroup/memory/gojudge and /sys/fs/cgroup/pids/gojudge and making them readable by the execution user, or by running with sudo. The topics also list cgroup2 and jobobject, which is the Windows equivalent of the same mechanism. On top of the resource limits, the seccomp filter is what produces the Dangerous Syscall status, and the repository has a seccomp/ directory for it. The Docker invocation in the README reflects all of this, since it runs the container with --privileged. That is a real operational cost, and it is the reason the warning about Windows and macOS matters, because those backends are documented as experimental and should not be used in production environments.

## Install is a release binary or a privileged container

The quick start offers two routes. The first is to download the compiled go-judge executable for your platform from the releases page and run it. The second is the container:

```bash
docker run -it --rm --privileged --shm-size=256m -p 5050:5050 --name=go-judge criyle/go-judge
```

The flags in that line are the requirements stated in one command: --privileged so the sandbox can create its cgroups and namespaces, --shm-size=256m so the in-memory file store has room, and the port mapping that exposes the default 5050 listener. The prerequisites section says the same thing in prose, with a Linux kernel version of 3.10 or newer and the cgroup file system mounted at /sys/fs/cgroup, which is usually done by systemd. There is also a second binary, go-judge-shell, which you download from the same releases page; it connects to a local go-judge and opens an interactive shell in the container, which is the tool to reach for when a program fails and you need to look inside the sandbox yourself.

## The alternative is a plain container, without the judging semantics

The realistic alternative to go-judge is running the program in a container yourself, with a container runtime, a resource limit and a timeout, and reading the exit status. The difference in approach is what the extra machinery buys. A plain container gives you isolation, and you can enforce a memory cap and a wall clock limit, but the failure you observe is a number, and a program killed at the limit looks the same as a program that crashed on its own unless you decode the signal yourself. go-judge decodes it for you, and it adds the pieces a judging system needs around the isolation: the file store with file ids and a time to live, the copy-in and copy-out limits, the output limit, the environment pool that reuses prepared sandboxes, the status classification, and the API with a REST and a gRPC surface, plus Prometheus metrics and a token authentication option. The price is the privilege requirement, the cgroup setup and the fact that you are now running a service with a network listener rather than a command. For a system that needs verdicts, that is the cheaper side of the trade.

## Conclusion

Adopt go-judge if you need to execute code you do not trust and want a verdict that distinguishes a timeout, a memory overrun, a blocked syscall and a missing input file, because that classification is the part that is hard to build on top of a plain container runtime. Do not adopt it if you only need to run a container, since the isolation primitives it uses are the same ones you would reach for yourself, and do not put it in production on Windows or macOS, which the README marks as experimental. Before you deploy, confirm the host has a cgroup filesystem mounted at /sys/fs/cgroup, plan for the privilege requirements, and decide how you will expose the API, since the default binding is localhost only and the gRPC endpoint is off by default.

## FAQ

### What is go-judge and what does it return?

It is a sandbox service built on Linux container technologies with a simple REST and gRPC API, running programs in a restricted environment. It returns a judged status such as Accepted, Memory Limit Exceeded, Time Limit Exceeded, Output Limit Exceeded, File Error, Non Zero Exit Status, Signalled, Dangerous Syscall or Internal Error, each with a documented reason.

### How do I run go-judge with Docker?

The README gives docker run -it --rm --privileged --shm-size=256m -p 5050:5050 --name=go-judge criyle/go-judge. The privileged flag is needed so the sandbox can create its cgroups, and the shared memory size is what backs the in-memory file store.

### What does go-judge need on the host?

A Linux kernel of version 3.10 or newer, and the cgroup file system mounted at /sys/fs/cgroup, which is usually done by systemd. With cgroup v1 the service also needs root privilege to create its cgroup sub-directories, unless it is run with sudo.

### Is go-judge production ready on Windows and macOS?

No. The README carries a warning that Windows and macOS support are experimental and should not be used in production environments. The architecture diagram shows those platforms as separate backends, winc on Windows and the app sandbox on macOS, alongside the Linux implementation.

### How do I enable the gRPC API and secure the REST API?

The gRPC endpoint is disabled by default and is enabled with the -enable-grpc flag, with its own default binding address of localhost:5051 and a -grpc-addr flag. Token based authentication for both REST and gRPC is added with the -auth-token flag.

## Sources

- [criyle/go-judge on GitHub](https://github.com/criyle/go-judge)
- [License: MIT](https://github.com/criyle/go-judge/blob/master/LICENSE)
- [Project website](https://docs.goj.ac)
- [README](https://github.com/criyle/go-judge/blob/master/README.md)
- [Releases](https://github.com/criyle/go-judge/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/criyle-go-judge
