# ureq chose blocking I/O on purpose, and the feature list is where the price shows

> The Rust HTTP client is built on the standard library's HTTP types, forbids unsafe code, and uses blocking rather than async I/O so the API stays small and the dependency tree stays short. Everything else interesting is in the feature flags: TLS has three mutually awkward backends, and the default one is explicitly not guaranteed to stay the default.

**algesten/ureq** — A simple, safe HTTP client

- Repository: https://github.com/algesten/ureq
- Stars: 2,202 · Forks: 234
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/algesten-ureq

## Blocking I/O is a decision, and the readme argues for it

The design paragraph is four sentences and each one is a choice. The library is pure Rust, for safety and ease of understanding, and it forbids unsafe code entirely. It uses blocking I/O instead of async I/O, and the reasons given are that this keeps the API simple and keeps dependencies to a minimum. It is based on the API of the standard library's HTTP crate. Those three constraints eliminate most of the design space that a Rust HTTP client usually occupies, and the results are worth naming. Without async, a request call is a function you call and a response you read, with no future to poll and no runtime to choose. The connection reuse story is a pool inside an agent rather than a task you spawn. And the dependency tree is small enough that a binary using it is easy to audit, which matters more for a client than the readme's casual phrasing suggests. For a tool that makes a few dozen requests, this is the right trade. For a server handling thousands of concurrent requests, blocking calls occupy runtime threads and you would need one thread per in-flight request, which is exactly the problem an async client exists to avoid. The readme does not hedge on this, which is a point in its favour: it is a client for one shape of workload and says so.

## The agent is where configuration lives, and it clones cheaply

The simplest call in the readme is four chained operations: a module-level get, a header, a call, and a read of the body to a string.

```rust
let body: String = ureq::get("http://example.com")
    .header("Example-Header", "header value")
    .call()?
    .body_mut()
    .read_to_string()?;
```
 That is the whole API for a one-off request, and it is worth noticing what is absent. There is no client object to construct, no configuration to set, and no handle to keep. For anything repeated, the readme introduces an agent, and its properties are the ones that matter. An agent holds a connection pool for reuse and a cookie store if the cookies feature is on. It is cheap to clone because of internal reference counting, and all clones share state, which means a clone is a handle rather than a copy and a cookie set in one is visible in the other. And creating one is how you set options, including the TLS configuration. The example builds configuration through a builder, sets a global timeout, converts it into an agent, and then makes two requests with the second comment noting it reuses the connection from the first. The global timeout is the option to know about, because without it a request to an unresponsive host can wait on the operating system default, and a tool that fans out requests needs a bound. The distinction between the module-level convenience functions and an agent is the same distinction most HTTP clients draw between a global default and a configured instance, and here the default is deliberately thin.

## Status codes are errors by default, which will surprise you once

Error handling has a section and it describes a default that catches people out. Errors come back as a result type that includes I/O errors, protocol errors, and, by default, HTTP status code errors, meaning a server responding with a client or server error status produces an error rather than a response. The readme notes this can be turned off through a named function on the configuration. There is a matching example that matches on the result and handles the status-code case separately from other failures. Two things are worth saying about this design. First, it is a considered choice rather than an oversight, and it is defensible: the most common bug in code using an HTTP client is forgetting to check the status, and making a 500 an error removes the possibility of forgetting. Second, it means the shape of your code differs from other clients, because the success path contains only 2xx and 3xx responses and everything else is in the error branch. If you are migrating code that treats a not-found response as ordinary, you will find that branch suddenly reachable. Turning the behaviour off is a configuration call, not a fork, which is the right way to offer it. The error type also distinguishes transport-level failures from protocol failures from status failures, so a caller who cares can tell a DNS failure from a timeout from a 503, and the readme's example shows the middle case without documenting the rest of the variants.

## Feature flags as a design tool, and what a minimal build costs you

The feature list is the longest part of the readme and it is a design document in disguise. Only two features are on by default, a TLS implementation and gzip. Everything else is opt-in, and the stated reason is to keep the dependency tree minimal. The list covers the things a real client needs and the readme is specific about each: cookies, three compression codecs, character set interpretation, JSON in both directions, multipart form submission, proxy configuration through four SOCKS URL schemes, and two different root certificate sources. Two of the descriptions carry more weight than the others. The character set feature exists because without it the library assumes UTF-8, and a response declaring a legacy single-byte encoding will be decoded incorrectly, which is a real interoperability problem with older servers. And the JSON feature is conditional on the cookies feature for its serialisation dependency, meaning enabling cookies changes the JSON stack slightly, which is the kind of coupling that surprises people who enable features independently. There is also a documentation-metadata block in the manifest that enables almost everything at once for building the published documentation, so the docs show the full surface rather than the default one. Read the feature list as the honest answer to what this client does not do unless asked, and check it against your requirements before you add the dependency rather than after.

## Three TLS paths and an explicit warning about the default

The TLS section is the most carefully worded part of the documentation and deserves to be read rather than skimmed. There is a pure-Rust implementation, which is the default, currently backed by a specific cryptographic provider library, and the readme explains why: as of a date in 2024 that provider has a higher chance of compiling successfully, and if a user installs a different provider in their process, that choice is respected. Then comes the sentence that matters: the library does not guarantee to default to that provider indefinitely, the feature flag will always work, but the specific crypto backend might change in a minor version. That is an unusually candid statement, and it has a direct consequence for you. A minor version upgrade can change your TLS implementation, which means a build that worked can fail to compile, and a supply chain reviewer comparing two builds has to account for it. The second path is a platform verifier, which uses whatever certificate verification the host operating system provides, which is what you want when you need to trust a corporate certificate store. The third is the platform TLS library, and the readme explains why it is never a default: to avoid the risk of a diamond dependency accidentally switching on an unwanted TLS implementation. So the native path must be configured explicitly on an agent, which is a small inconvenience in exchange for the guarantee that a transitive dependency cannot change your transport. Three ways to do TLS is a consequence of living in an ecosystem where TLS is still settling, and the readme handles it as a fact rather than a problem.

## A dual licence, a generated readme, and a migration document

The repository layout is small and each entry means something. There are two licence files, matching the manifest's dual licensing of the crate under either of two permissive licences, which is the standard Rust convention and means you pick. There is a changelog linked from the readme for release details, and a release notes file. There is a contributing guide. There is a lock file, which is normal for a repository that builds its own tests but unusual to commit in a library, and it pins the tree that the test suite runs against. There is a dependency-policy configuration and a shell script that runs the checking tool it configures, which is a supply chain measure: the project audits what it depends on and fails on a violation. There is an examples directory with three files, one of which is a small tool built with the library, one showing how to plug in a different transport over a channel, and one about proxies, and the transport example is the most interesting of the three because it shows the client is not welded to a particular I/O implementation. There is a template file for the readme, and the readme itself begins with a comment recording that it is generated from the library source by a specific tool invocation. Documentation generated from the code cannot drift from it, which is the right trade for a project whose readme is a feature list, and it also means the readme's structure is a choice the library author has already made for you.

## Conclusion

Adopt ureq when you are writing a Rust program that makes a modest number of HTTP calls, especially a command line tool, a script or a service whose request volume does not justify an async runtime, because the readme's stated reason for blocking I/O is a smaller API and a smaller dependency tree, and that is a good reason. Do not adopt it for a high-concurrency server where you would be blocking a runtime worker thread on every request. Four things to verify. Which TLS backend your build ends up with, because the readme says the default is not guaranteed indefinitely and that the specific crypto backend might change in a minor version, so a minor upgrade can change your transport. Whether you need features that are off by default, since only two are on and the rest are opt-in, which is where a surprising missing method usually comes from. That the crate version matches your ecosystem, because the current major is three and there is a migration document from two to three at the repository root, so a codebase on two will need work. And what your minimum compiler version is, since the package declares one. The manifest offers a dual licence, MIT or Apache-2.0, and the readme is generated from the library source by a tool whose command is recorded in a comment at the top of the file.

## FAQ

### Why does ureq use blocking I/O instead of async?

The readme says it uses blocking I/O because that keeps the API simple and keeps dependencies to a minimum. The library is built on the API of the standard library's HTTP crate and forbids unsafe code, so a request is a call you make rather than a future you poll.

### Which features does ureq enable by default?

Two: a TLS implementation and gzip. Cookies, character set handling, JSON, multipart, SOCKS proxying, brotli, and root certificate sources are all opt-in, and the readme gives the dependency line for choosing them when you add the crate.

### How does ureq handle HTTP error status codes?

By default a 4xx or 5xx response is returned as an error rather than as a response, alongside I/O and protocol errors. The readme shows matching on the error to separate a status-code failure from a transport failure, and says the behaviour can be turned off through a configuration function.

### Which TLS backends does ureq support and which is the default?

Three: a pure-Rust implementation with a specific cryptographic provider, which is the default; a platform verifier that uses the host operating system's certificate checking; and the platform TLS library, which is never a default and must be configured on an agent. The readme warns that the default provider is not guaranteed to stay the same and could change in a minor version.

### What licence is ureq released under?

Dual licensed, MIT or Apache-2.0, with both licence files in the repository root and the manifest declaring the same pair. The crate version is 3.4.2 on the 2024 edition, with a minimum compiler version declared, and a migration document from version 2 to version 3 at the root.

## Sources

- [algesten/ureq on GitHub](https://github.com/algesten/ureq)
- [Issues](https://github.com/algesten/ureq/issues)
- [License: Apache-2.0](https://github.com/algesten/ureq/blob/main/LICENSE)
- [README](https://github.com/algesten/ureq/blob/main/README.md)

---

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