gor-cli 0.2.0

A Rust CLI for GitHub — a 'gh' clone
Documentation
---
number: 4
title: Sync-by-default concurrency model using reqwest blocking
status: accepted
date: 2026-07-16
tags: [async, architecture, dependencies]
deciders: [kwhatcher]
---

# Sync-by-default concurrency model using reqwest blocking

## Context and Problem Statement

gor is a CLI tool that makes HTTP requests to the GitHub API. Rust offers two concurrency models: synchronous (blocking I/O) and asynchronous (non-blocking I/O with an async runtime like Tokio). Which model should gor adopt as its primary approach?

## Decision Drivers

* **Simplicity** — synchronous code is easier to write, debug, and test
* **Startup time** — async runtimes add initialization overhead
* **Dependency footprint** — Tokio is a large dependency
* **Concurrency needs** — gor is a batch CLI tool; most commands make sequential API calls
* **Ecosystem alignment**`reqwest::blocking` is well-supported and widely used

## Considered Options

* Synchronous with `reqwest::blocking` — blocking HTTP client, no async runtime
* Async with Tokio + `reqwest` — non-blocking HTTP client with Tokio runtime
* Hybrid — sync core library with async shell when needed

## Decision Outcome

Chosen option: **Synchronous with `reqwest::blocking`**, because gor is a batch CLI tool that does a task and exits. The simplicity of synchronous code outweighs the potential performance benefits of async for gor's use case. If parallel I/O is needed later, targeted async or `rayon` can be added without converting the entire codebase.

### Consequences

* Good, because simpler code — no `async`/`await`, no `Pin`, no `Send + Sync` bounds
* Good, because easier debugging — synchronous stack traces are straightforward
* Good, because faster startup — no Tokio runtime initialization
* Good, because smaller binary — no Tokio dependency
* Good, because `reqwest::blocking` is a thin wrapper around the async client, so switching later is feasible
* Bad, because sequential API calls are slower than concurrent ones for multi-entity operations
* Bad, because no built-in timeout/reactor — must manage timeouts manually
* Bad, because blocking I/O can stall if a request hangs without a timeout

### Confirmation

All HTTP client code in `src/client.rs` uses `reqwest::blocking::Client`. There is no `tokio` or `async` code in the codebase. CI verifies this via `cargo deny check` (no Tokio in the dependency graph unless explicitly needed later).

## Pros and Cons of the Options

### Synchronous with reqwest::blocking

Blocking HTTP client with no async runtime.

* Good, because simple, linear code that is easy to reason about
* Good, because no async runtime dependency — smaller binary, faster startup
* Good, because `reqwest::blocking` is a thin wrapper; switching to async is feasible
* Good, because easier to test — no need for `#[tokio::test]` or async test harnesses
* Neutral, because `reqwest::blocking` internally uses a Tokio runtime (hidden)
* Bad, because sequential requests are slower than concurrent ones
* Bad, because blocking on I/O can stall the entire process

### Async with Tokio + reqwest

Non-blocking HTTP client with Tokio runtime.

* Good, because concurrent requests are faster for multi-entity operations
* Good, because built-in timeout and cancellation support
* Good, because `reqwest` (async) is the primary maintained variant
* Bad, because adds Tokio as a dependency (~20+ crates)
* Bad, because async code is harder to debug and test
* Bad, because `Send + Sync` bounds propagate through the codebase
* Bad, because slower startup due to runtime initialization

### Hybrid

Sync core library with async shell when needed.

* Good, because keeps business logic synchronous and testable
* Good, because async can be added incrementally for specific commands
* Neutral, because requires careful API design to support both modes
* Bad, because two I/O patterns in one codebase increase complexity
* Bad, because still requires Tokio as a dependency

## More Information

This decision follows the principle "async is an optimization, not an architecture" from Microsoft's Rust training materials. If gor later needs concurrent API calls (e.g., for `run watch` polling multiple workflow runs), the recommended approach is:

1. Keep the core library synchronous
2. Add a targeted Tokio runtime in the specific command handler
3. Use `tokio::task::spawn_blocking` for any CPU-bound work

This decision should be revisited if:
- A command requires 5+ concurrent API calls for acceptable performance
- Streaming responses (SSE, WebSocket) are needed
- The `reqwest::blocking` variant is deprecated