Judex is a small distributed code execution backend for online judge style systems. It accepts source code over HTTP, queues submissions in Redis, and executes them asynchronously inside restricted Docker sandboxes for Python, C, C++, Go, and Java.
The project is useful as a reference implementation for building the backend pieces behind coding platforms: API submission, queueing, worker pools, container isolation, rate limiting, structured logs, metrics, and local observability.
Security note Judex is designed as a learning and development project, not a hardened multi-tenant sandbox. Review the Docker isolation model, host Docker socket exposure, resource limits, and operational controls before running untrusted code in production.
- Features
- Architecture
- Project Structure
- Prerequisites
- Quick Start
- Configuration
- API Reference
- Docker Setup
- Observability
- Development
- Testing
- Asynchronous submission flow with a Redis-backed pending and processing queue.
- API and worker services that can be scaled independently.
- Warm Docker container pools per language to avoid creating a fresh container for every job.
- Redis-backed distributed token bucket rate limiting for submission requests.
- Per-job result storage in Redis with JSON status records.
- Stuck job recovery for jobs left in the processing queue.
- Completed job cleanup in the worker process.
- Prometheus metrics for HTTP and execution timings.
- Docker Compose stack for Redis, API, worker, Prometheus, Grafana, Loki, and Promtail.
flowchart LR
Client["Client / Frontend"]
API["API service<br/>:8080"]
RateLimit["Redis token bucket<br/>10 burst, 1 token/sec"]
Redis[("Redis<br/>job store + queues")]
Worker["Worker service<br/>4 goroutines<br/>metrics :8081"]
Docker["Docker Engine"]
Metrics["Prometheus<br/>:9090"]
Logs["Loki + Promtail<br/>:3100"]
Grafana["Grafana<br/>:3000"]
Client -->|"POST /judex/run"| API
Client -->|"GET /judex/result/{id}"| API
Client -->|"GET /health"| API
API --> RateLimit
RateLimit <--> Redis
API -->|"enqueue job"| Redis
Worker -->|"BLMOVE pending -> processing"| Redis
Worker -->|"update result"| Redis
Worker --> Docker
Docker --> Python["judex-python"]
Docker --> C["judex-c"]
Docker --> CPP["judex-cpp"]
Docker --> Go["judex-go"]
Docker --> Java["judex-java"]
Metrics --> API
Metrics --> Worker
Logs --> API
Logs --> Worker
Grafana --> Metrics
Grafana --> Logs
sequenceDiagram
participant Client
participant API
participant Redis
participant Worker
participant Sandbox as Warm sandbox container
Client->>API: POST /judex/run
API->>Redis: EVAL token bucket script
alt rate limited
API-->>Client: 429 Rate limit exceeded
else accepted
API->>Redis: SET job:{id} status=pending
API->>Redis: LPUSH pending_jobs
API-->>Client: 200 { job_id, status }
end
Worker->>Redis: BLMOVE pending_jobs -> processing_jobs
Worker->>Redis: SET job:{id} status=running
Worker->>Sandbox: upload source file
Worker->>Sandbox: compile and/or run command
Sandbox-->>Worker: stdout, stderr, status
Worker->>Redis: SET job:{id} final result
Worker->>Redis: LREM processing_jobs
Client->>API: GET /judex/result/{job_id}
API->>Redis: GET job:{id}
API-->>Client: job status and result
The worker creates warm containers for every supported language during startup. Jobs borrow a container from the language pool, upload source into /workspace, execute the configured compile/run command, then sanitize the container before returning it to the pool.
Current sandbox settings:
| Setting | Value |
|---|---|
| Container user | 1000 |
| Memory limit | 256 MiB |
| CPU limit | 1 CPU |
| PID limit | 64 |
| Network | disabled (none) |
| Linux capabilities | dropped (ALL) |
| Security option | no-new-privileges |
| Per-job context timeout | 30s |
.
├── cmd/
│ ├── api/ # HTTP API entrypoint
│ └── worker/ # worker service entrypoint
├── docker/ # sandbox images for each language
│ ├── c/
│ ├── cpp/
│ ├── go/
│ ├── java/
│ └── python/
├── internal/
│ ├── app/ # application wiring
│ ├── cleanup/ # completed job cleanup
│ ├── executor/ # language-specific compile/run logic
│ ├── handler/ # HTTP handlers
│ ├── limiter/ # Redis Lua token bucket
│ ├── middleware/ # CORS, logging, metrics, request IDs
│ ├── metrics/ # Prometheus collectors
│ ├── queue/ # Redis queue and recovery logic
│ ├── sandbox/ # Docker warm container pool
│ ├── store/ # Redis job storage
│ └── worker/ # worker pool and job processor
├── tests/ # shared test helpers
├── docker-compose.yml # local runtime and observability stack
├── prometheus.yml # Prometheus scrape config
├── loki-config.yaml # Loki config
├── promtail-config.yaml # Promtail Docker log discovery
├── Dockerfile # API/worker service image
└── Makefile # common development commands
- Go 1.25+
- Docker Engine with access to
/var/run/docker.sock - Docker Compose v2
- Redis 7+ if running services outside Compose
make- Optional:
golangci-lintformake lint
Clone the repository and install Go dependencies:
git clone https://github.com/Dharshan2208/judex.git
cd judex
go mod downloadBuild the language sandbox images:
make imagesStart Redis:
docker run --rm --name judex-redis -p 6379:6379 redis:7-alpineStart the API and worker in separate terminals:
make run-apimake run-workerSubmit a Python job:
curl -sS -X POST http://localhost:8080/judex/run \
-H 'Content-Type: application/json' \
-d '{"language":"python","code":"print(\"hello from judex\")"}'Poll the result:
curl -sS http://localhost:8080/judex/result/<job_id>Judex reads environment variables directly and also attempts to load a local .env file.
| Variable | Default | Used by | Description |
|---|---|---|---|
REDIS_ADDR |
localhost:6379 |
API, worker | Redis address. Compose sets this to redis:6379. |
Example .env:
REDIS_ADDR=localhost:6379The following values are currently hard-coded in the application:
| Setting | Value | Location |
|---|---|---|
| API port | 8080 |
cmd/api/main.go |
| Worker metrics port | 8081 |
cmd/worker/main.go |
| Worker count | 4 |
internal/app/app.go |
| Queue capacity | 1000 |
internal/app/app.go |
| Rate limit | burst 10, refill 1/sec |
cmd/api/main.go |
| Redis job TTL | 24h |
internal/store/redis_store.go |
| Completed job cleanup age | 15m |
cmd/worker/main.go |
| Stuck job recovery age | 5m |
cmd/worker/main.go |
Submit a source file for asynchronous execution.
Request:
{
"language": "python",
"code": "print(\"hello from judex\")"
}Supported language values:
| Language | Value | Source filename |
|---|---|---|
| Python | python |
main.py |
| C | c |
main.c |
| C++ | cpp |
main.cpp |
| Go | go |
main.go |
| Java | java |
Main.java |
Response:
{
"job_id": "6d9b58ec-d381-4af4-a837-80aa3e13a8c9",
"status": "pending"
}Possible errors:
| Status | Reason |
|---|---|
400 |
Invalid JSON body |
405 |
Method is not POST |
429 |
Rate limit exceeded or queue full |
Fetch job state and execution output.
curl -sS http://localhost:8080/judex/result/6d9b58ec-d381-4af4-a837-80aa3e13a8c9Example completed response:
{
"id": "6d9b58ec-d381-4af4-a837-80aa3e13a8c9",
"language": "python",
"status": "completed",
"created_at": "2026-07-05T12:00:00Z",
"claimed_at": "2026-07-05T12:00:01Z",
"completed_at": "2026-07-05T12:00:01Z",
"result": {
"stdout": "hello from judex\n",
"stderr": "",
"status": "success",
"language": "python",
"execution_time_ms": 42
}
}Possible job statuses:
| Status | Meaning |
|---|---|
pending |
Accepted and waiting in the queue |
running |
Claimed by a worker |
completed |
Execution finished successfully |
compile_error |
Compilation failed for a compiled language |
runtime_error |
Runtime execution failed |
timeout |
Execution exceeded the active timeout path |
internal_error |
Worker or sandbox setup failed |
unsupported language |
No executor exists for the submitted language |
Possible HTTP errors:
| Status | Reason |
|---|---|
404 |
Job ID was not found or has expired/been cleaned up |
405 |
Method is not GET |
Return API process health and queue counters.
curl -sS http://localhost:8080/health{
"status": "ok",
"queue_length": 0,
"queue_capacity": 1000,
"submitted_jobs": 1,
"completed_jobs": 0,
"failed_jobs": 0
}Expose Prometheus metrics.
curl -sS http://localhost:8080/judex/metrics
curl -sS http://localhost:8081/judex/metricsRun the full local stack:
make upStop it:
make downCompose starts:
| Service | Port | Purpose |
|---|---|---|
api |
8080 |
Judex HTTP API |
worker |
8081 |
Worker Prometheus metrics |
redis |
6379 |
Queue, job store, rate limiter |
prometheus |
9090 |
Metrics scraping |
grafana |
3000 |
Dashboards and log exploration |
loki |
3100 |
Log storage |
promtail |
9080 internal |
Docker log discovery and shipping |
The worker mounts /var/run/docker.sock so it can create sibling sandbox containers on the host Docker engine. It also mounts /app/temp:/app/temp, although current code uploads source directly into warm containers through the Docker API.
Before running only the worker outside Compose, build sandbox images locally:
make imagesPrometheus scrapes:
- API metrics from
api:8080/judex/metrics - Worker metrics from
worker:8081/judex/metrics
Application metrics currently include:
| Metric | Labels | Description |
|---|---|---|
judex_request_duration_seconds |
endpoint, status |
HTTP request latency |
judex_execution_duration_seconds |
language |
Total language execution latency |
judex_compile_duration_seconds |
language |
Compile step latency |
judex_run_duration_seconds |
language |
Run step latency |
Promtail discovers Docker containers through the Docker socket and ships logs to Loki with labels such as service, container_name, and compose_project.
Grafana is available at:
http://localhost:3000
Common commands:
make help
make images
make run-api
make run-worker
make test
make vet
make lint
make cleanBuild production binaries locally:
go build -o bin/api ./cmd/api
go build -o bin/worker ./cmd/workerRun them:
REDIS_ADDR=localhost:6379 ./bin/apiREDIS_ADDR=localhost:6379 ./bin/worker- Add a sandbox image under
docker/<language>/Dockerfile. - Add the image name to
IMAGESinMakefile. - Register the language-to-image mapping in
internal/app/app.go. - Implement an executor in
internal/executor/. - Add the language case in
Worker.getExecutor. - Add tests for executor behavior and handler/worker integration.
- Update this README's supported language table.
Run the full Go test suite with race detection:
make testRun go vet:
make vetRun linting, if golangci-lint is installed:
make lintThe test suite uses helpers under tests/, including Redis test helpers and fake Docker behavior for sandbox-adjacent tests.
This project is licensed under the AGPL-3.0 License. See LICENSE for details.