Repository: buchgr/bazel-remote
Pull request #386 (2021-01-03 17:08:21)

Add support for the compressed-blobs REAPI proposal
This PR adds support for the [compressed-blobs REAPI proposal](https://github.com/bazelbuild/remote-apis/pull/168).

By default, uploaded CAS blobs are stored in a file with a header and zstandard-compressed chunks (corresponding to 1M of raw data, initially). When clients request one of these items in zstandard compressed form, bazel-remote can stream data without recompression (or if the client requests data from an offset, at most one chunk will be recompressed).

Preexisting CAS blobs are not migrated to the compressed format, because this can take a lot of time for large caches. Instead, these blobs are moved into the new cache directory structure with a .v1 filename extension. If the storage mode is set to "uncompressed" then CAS blobs are always stored in this form. (I plan to investigate on-the-fly recompression for preexisting blobs later.)

The HTTP interface also supports zstandard compressed blobs via `Accept-Encoding: zstd` for GET and `Content-Encoding: zstd` and a custom `X-Digest-SizeBytes` header for PUT.

Implements #12.

mostynb: @glukasiknuro, @ulrfa, @bdittmer, @buchgr: FYI- this is a significant change.

Once this lands and stabilises, I'll make a 2.0.0 release.
mostynb: > Interesting @mostynb! I think the network bandwidth saving can be beneficial for my use cases! 🎉
> 
> Do you know if the bazel client, and others such as buildbarn, have plans to implement or make use of this feature?

Bazel plans to implement this: https://github.com/bazelbuild/bazel/issues/12670
It looks like buildfarm might too: https://github.com/bazelbuild/bazel-buildfarm/issues/504

I don't think buildbarn is interested in implementing this for now (but I will offer to contribute patches later).

> Do you believe clients will typically use zstd for **all** their CAS uploads/downloads (including small files)? Do you expect transcoding to be common?

It's entirely up to the client, so hard to predict (this is why I added a storage mode config option). However zstandard is fast enough that it might be OK to do a lot of transcoding, and there is still some benefit to be had from the increased effective cache size.

I suspect that bazel will add a simple on/off setting for using compressed-blobs, and maybe stop there.

> However, I wonder about reading from offset in compressed files. **What use cases do you have in mind for that?** It seems to me that it comes with drawbacks:
> 
> A) Need for header in each file.
> B) Re-compression when receiving already compressed files via HTTP PUT.
> C) Complexity.
> D) Performance overhead (due to A and B).

The use case is for resuming interrupted downloads of large blobs, without resorting to downloading in uncompressed form or decompressing the entire blob + recompressing the tail. With the chunked format, at most one chunk needs to be recompressed.

The server needs to decompress the blob after receiving it in order to verify the hash anyway, so it's just one extra compression, which should be OK if blobs are likely to be read multiple times.

> For me it would be attractive to have the files stored without the extra header, and instead with a .zst file name extension (RFC 8478). Primarily for performance reasons, but also to allow digging into the cache directories with standard zstd command line tools. The logical size could also be embedded in the file name if needed.

There is a seekable zstandard format, but as far as I know nothing supports it yet. I would consider migrating to that later if it takes off.

In the meantime if needed we could provide a small utility which strips the header from a blob (the concatenated chunks can be processed by standard tools).

> Have you run performance benchmarks?

Transferring large compressible blobs is much faster (of course), and I see cache sizes about 30% of the uncompressed size in my test cases (this will vary for different workloads).
ulrfa: > > However, I wonder about reading from offset in compressed files. **What use cases do you have in mind for that?** It seems to me that it comes with drawbacks:
> > A) Need for header in each file.
> > B) Re-compression when receiving already compressed files via HTTP PUT.
> > C) Complexity.
> > D) Performance overhead (due to A and B).
> 
> The use case is for resuming interrupted downloads of large blobs, without resorting to downloading in uncompressed form or decompressing the entire blob + recompressing the tail. With the chunked format, at most one chunk needs to be recompressed.

Are people often experiencing interrupted downloads of huge files from CAS?  (Personally, I’m not)

I wonder how the performance is affected with huge amount of concurrent HTTP requests? I would be happy to run benchmarks with 72 core machines and 20 Gbps network. But how to best do that now without zstd support in bazel client?

Would it make sense to benchmark the following scenarios?

- A. Non-compressed HTTP GET & PUT requests from bazel clients, to bazel-remote --storage_mode zstd

   - In order to detect overhead by transcoding
   - In order to detect overhead by io.Copy not being able to optimize with sendfile(2) since source is not an *os.File regular file.

- B. Non-compressed HTTP GET & PUT requests from bazel clients, to bazel-remote --storage_mode uncompressed

   - In order to detect overhead by all the small file syscalls to read/write the header


mostynb: > Are people often experiencing interrupted downloads of huge files from CAS? (Personally, I’m not)

I don't have an answer to this, because IIRC it's possible to download uncompressed blobs from an offset and therefore I'm unlikely to get complaints about it.

> I wonder how the performance is affected with huge amount of concurrent HTTP requests? I would be happy to run benchmarks with 72 core machines and 20 Gbps network. But how to best do that now without zstd support in bazel client?

I think the best we can do in the meantime is try to put together an artificial stress-test setup using something like curl (recent versions support zstd downloads) or a custom client (which is fairly easy to write). Then we can ramp up the load with xargs or make or similar.

> Would it make sense to benchmark the following scenarios?
> 
> * A. Non-compressed HTTP GET & PUT requests from bazel clients, to bazel-remote --storage_mode zstd
> 
>   * In order to detect overhead by transcoding
>   * In order to detect overhead by io.Copy not being able to optimize with sendfile(2) since source is not an *os.File regular file.

That is doable today, with bazel configured to use remote cache via gRPC or HTTP. We should expect higher CPU and memory usage on the server, but hopefully a larger effective cache size.

> * B. Non-compressed HTTP GET & PUT requests from bazel clients, to bazel-remote --storage_mode uncompressed
>   
>   * In order to detect overhead by all the small file syscalls to read/write the header

There should be no extra load if `--storage_mode uncompressed` stores raw files with a ".v1" extension to distinguish them from the new header + compressed blob format. I added this for the "migrating existing files from the old directory structure" step, but I can't remember if I have added if for the Put code path yet. I'll follow up tomorrow.
mostynb: BTW, I am also thinking through some `--storage_mode hybrid` ideas, but it's not something I want to attempt in the first PR. For example:
* Store blobs in the form they were uploaded (uncompressed or in our compressed format).
* Compress and replace uncompressed blobs if/when a client requests them in compressed form.
mostynb: Interesting, maybe we could implement the header as a skippable frame (or multiple skippable frames), in which case the entire compressed file would be decodable by any compliant zstandard decoder: https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#skippable-frames

The cost compared to the current header would only be 8 bytes per file for most files.
mostynb: Pushed some new commits:
1) Make the compressed CAS blob file format regular .zst (the header is now a zstd skippable frame).
2) Include the logical size in the filename of compressed blobs, so we can import them from the directory listing alone (ie without reading the header of each blob).
3) Fix the uploaded CAS blob filenames for `--storage_mode uncompressed`.

The first two changes break compatibility with cache directories created with earlier versions of this branch/PR.
ulrfa: > BTW, I am also thinking through some `--storage_mode hybrid` ideas, but it's not something I want to attempt in the first PR. For example:
> 
> * Store blobs in the form they were uploaded (uncompressed or in our compressed format).

Interesting idea!

The client might have additional information to make decision if certain files are worth compressing or not, then it could make sense to also store them in CAS as they were uploaded.

Or bazel-remote could decide to not compress files smaller than 4 KB? Since those would still consume a whole file system block anyway.

Below follows an example of the file size distribution in one of our caches.

Note that 60% of the files in that CAS have size of 128 bytes or less. Those should not be compressed.

Also note that 98% of the files would fit in a single 1M chunk. For them the seekable zstd table is unnecessary.

```
Size  Number of files in CAS
   8:       6
  16:   47690
  32:     100
  64:     537
 128: 6471070
 256:    5044
 512:   64129
  1k:  183429
  2k:  320129
  4k:  998385
  8k: 1611520
 16k:  264389
 32k:  269138
 64k:  177826
128k:  109303
256k:   96537
512k:   61308
  1M:   22527
  2M:   70088
  4M:   62483
  8M:   55450
 16M:    5043
 32M:    1657
 64M:      34
256M:       2
```



mostynb: > > BTW, I am also thinking through some `--storage_mode hybrid` ideas, but it's not something I want to attempt in the first PR. For example:
> > 
> > * Store blobs in the form they were uploaded (uncompressed or in our compressed format).
> 
> Interesting idea!
> 
> The client might have additional information to make decision if certain files are worth compressing or not, then it could make sense to also store them in CAS as they were uploaded.
> 
> Or bazel-remote could decide to not compress files smaller than 4 KB? Since those would still consume a whole file system block anyway.

Note that there are two other costs to consider: network transfer time (even if we didn't save disk space, we might save transfer time) and re-compression time (eg if new clients request all blobs in compressed form). This is something that I think we could experiment with in the "hybrid" storage mode.

> Below follows an example of the file size distribution in one of our caches.

Interesting- can you share the language(s) in use here?

> Note that 60% of the files in that CAS have size of 128 bytes or less. Those should not be compressed.
> 
> Also note that 98% of the files would fit in a single 1M chunk. For them the seekable zstd table is unnecessary.

Right, it's an optimization for large blobs.
ulrfa: > Note that there are two other costs to consider: network transfer time (even if we didn't save disk space, we might save transfer time) and re-compression time (eg if new clients request all blobs in compressed form). This is something that I think we could experiment with in the "hybrid" storage mode.

Agree about experiments. For example I’m curious about how much the network transfer is affected by compressing small files that would have fit in a single 1500 byte ethernet MTU even as non-compressed.

Regarding re-compression: If bazel-remote decides to store some files uncompressed, then I assume bazel-remote would have the choice of transferring them uncompressed, even if client also accepts zstd encoding?

> > Below follows an example of the file size distribution in one of our caches.
> 
> Interesting- can you share the language(s) in use here?

I think it is mostly building C code and running tests with a proprietary test framework.

Many of the tiny files seems to be REv2 messages, such as Action messages that mainly contains only two Digests. That particular cache instance is not used for remote execution, but those messages are always **uploaded** by bazel client to CAS also for pure caching setups. However, they are only **downloaded** if using remote execution.

(Note that I mean the Action message in CAS, not the ActionResult message in AC)

One-liners for calculating size distribution: https://superuser.com/questions/565443/generate-distribution-of-file-sizes-from-the-command-prompt


mostynb: > > Note that there are two other costs to consider: network transfer time (even if we didn't save disk space, we might save transfer time) and re-compression time (eg if new clients request all blobs in compressed form). This is something that I think we could experiment with in the "hybrid" storage mode.
> 
> Agree about experiments. For example I’m curious about how much the network transfer is affected by compressing small files that would have fit in a single 1500 byte ethernet MTU even as non-compressed.
> 
> Regarding re-compression: If bazel-remote decides to store some files uncompressed, then I assume bazel-remote would have the choice of transferring them uncompressed, even if client also accepts zstd encoding?

That is possible for HTTP (with some refactoring) because the server has a way to specify the encoding, but not for gRPC bytestream where the server only returns data (not metadata). It might be something to reconsider for REAPIv3.

> > > Below follows an example of the file size distribution in one of our caches.
> > 
> > 
> > Interesting- can you share the language(s) in use here?
> 
> I think it is mostly building C code and running tests with a proprietary test framework.
> 
> Many of the tiny files seems to be REv2 messages, such as Action messages that mainly contains only two Digests. That particular cache instance is not used for remote execution, but those messages are always **uploaded** by bazel client to CAS also for pure caching setups. However, they are only **downloaded** if using remote execution.

We could try using heuristics in a future hybrid storage mode. For example, any CAS blob under a certain size is stored uncompressed, and then only compressed for storage if it is downloaded a certain number of times (between bazel-remote restarts).

> (Note that I mean the Action message in CAS, not the ActionResult message in AC)

I can't think of a reason why the client would upload Action messages for cache-only mode. My guess is that it either simplifies the code on the client side, or it's a bug.
ulrfa: > I can't think of a reason why the client would upload Action messages for cache-only mode. My guess is that it either simplifies the code on the client side, or it's a bug.

I'm not sure, but one reason could be the following, which is valuable for me:

Sometimes I analyze content in ac & cas, by starting from the ActionResult and following references from Action to Command and further, to get the associations between output files in CAS and their originating commands. In order answer questions like:

- How many alternative compiled object files are there in the CAS for a particular source file?
- What changes typically cause cache misses?
- Are there many similar commands resulting in same output file?
- ...

I have only done it in small caches, but I have ideas about a tool for creating index mapping original action output file names to Action digests. Then it hope being able to see patterns also in large caches. I though about generating such index from within bazel-remote, but I would like to avoid that overhead and instead only generating it on demand when needed.
mostynb: The proxy backends should work now, and I'm not aware of any other blockers for landing this. There is one limitation that might be annoying- it's not possible to use storage_mode zstd and proxy to another bazel-remote instance via http, but I think that can be implemented later.
tschuett: Thanks for doing this.
How about a second small lru cache. When somebody reads from an offset of a Zstd file, uncompress the file, put the uncompressed file into the second cache, and serve the data to the client. If somebody else reads from an offset from the same file, you can serve the data from the second cache.
mostynb: @tschuett: I have some ideas for "hybrid" storage modes that we can experiment with later, which might cover that scenario if it occurs often enough. Hopefully reads from offsets are rare though.
tschuett: Thanks. It will be interesting to see the actual Bazel implementation.
mostynb: @ulrfa: I decided to try to squeeze in one last change while changing the on-disk format. The latest commit uses psuedo-random strings in the blob filenames, so there's no canonical file for each hash that needs to be replaced while holding the LRU index lock.

To avoid incomplete files being used when restarting bazel-remote, I use the setgid permission bit- it's set when creating the file and un-set once the file has been written successfully. Then on startup we discard any files with the setgid bit set.
ulrfa: > @ulrfa: I decided to try to squeeze in one last change while changing the on-disk format. The latest commit uses psuedo-random strings in the blob filenames, so there's no canonical file for each hash that needs to be replaced while holding the LRU index lock.

@mostynb, thanks for considering the on-disk format for the locks!

I wonder what happens in the following scenario, with two PUT and one GET request, all with the same key:

**Request1-PUT**: Writes file and commits to LRU.
**Request2-GET**: lru.Get returns item.random. But Request3 arrives before Request2 calls os.Open.
**Request3-PUT**: Writes file and commits to LRU. Will it replace the previous entry in LRU? Is that triggering an eviction of the file from Request1?
**Request2-GET**: Continues with os.Open. Will it fail to open the file from request1?

mostynb: `availableOrTryProxy` opens the file while holding the LRU lock and returns the `*os.File`, and the lock is also held whenever evicting items (somewhere inside `Put` or `Get` with a proxy download).

So request 2 gets the first version of the file and has a valid/usable file handle, then that file is unlinked due to request 2.
ulrfa: > `availableOrTryProxy` opens the file while holding the LRU lock

@mostynb, to me it seems like the mutex is released before opening the file. Right?

https://github.com/buchgr/bazel-remote/blob/33dfce63394645639b63fcbaf8f60023721f2c97/cache/disk/disk.go#L689-L696

I think we decided to release the lock before os.Open on purpose, to avoid holding the mutex while doing frequent filesystem syscalls. And we considered it OK that os.Open could fail if an **old** entry was evicted. But with the latest change also **recently** written files are deleted when replacing lru item with same key.

mostynb: > @mostynb, to me it seems like the mutex is released before opening the file. Right?
> 
> https://github.com/buchgr/bazel-remote/blob/33dfce63394645639b63fcbaf8f60023721f2c97/cache/disk/disk.go#L689-L696
> 
> I think we decided to release the lock before os.Open on purpose, to avoid holding the mutex while doing frequent filesystem syscalls. And we considered it OK that os.Open could fail if an **old** entry was evicted. But with the latest change also **recently** written files are deleted when replacing lru item with same key.

Right- good catch. There's also another bug which masks this one: I forgot to make `SizedLRU.Add` remove files when replacing items.

One solution to the first bug might be to try to open the file without holding the lock, and if the file does not exist anymore, try again while holding the lock.

For the second bug, we can either delete the old file while holding the lock, or spawn a goroutine to perform the deletion so we can continue and release the lock (possibly with a delay to increase the probability of hitting the fast path inside Get).

I'll think this through some more, maybe there's a better solution.
mostynb: Here's another potential solution, reference counting Gets that haven't opened the file yet:
* When adding an item to the LRU, initialise a uint32 counter in the lruItem value to 1 (while holding the lock).
* When the value is evicted from the LRU index, perform an atomic decrement and remove the file if the resulting uint32 is 0 (this would require some refactoring to perform the file removals without holding the lock - perhaps by spawning a new goroutine per file removal).
* When a Get for that item occurs, do an atomic increment, and defer a function which performs an atomic decrement and then removes the file if the resulting uint32 is 0 (the lock is released after deferring).

This will overflow if there are 4,294 million concurrent Gets (that haven't opened the file yet) for the same value, but I don't think we'd scale that high anyway.

However, I think it would be better to start with the fallback slow path fix I mentioned the other day. ie if we fail to open the file, perform another LRU lookup and open the file while holding the lock (and remove files when replacing items in the index). Maybe that will be fast enough.

Patch:
diff --git a/README.md b/README.md
index 17605c0..9b2e674 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ commodity hardware and AWS servers. Outgoing bandwidth can exceed 15 Gbit/s on t
 ## HTTP/1.1 REST API
 
 Cache entries are set and retrieved by key, and there are two types of keys that can be used:
-1. Content addressed storage (CAS), where the key is the lowercase SHA256 hash of the stored value.
+1. Content addressed storage (CAS), where the key is the lowercase SHA256 hash of the entry.
    The REST API for these entries is: `/cas/<key>` or with an optional but ignored instance name:
    `/<instance>/cas/<key>`.
 2. Action cache, where the key is an arbitrary 64 character lowercase hexadecimal string.
@@ -24,6 +24,14 @@ Cache entries are set and retrieved by key, and there are two types of keys that
 Values are stored via HTTP PUT requests, and retrieved via GET requests.
 HEAD requests can be used to confirm whether a key exists or not.
 
+If GET requests specify `zstd` in the `Accept-Encoding` header, then
+zstandard-encoded data may be returned.
+
+To upload zstandard compressed data, PUT requests must set
+`Content-Encoding: zstd` and include a custom `X-Digest-SizeBytes` header
+with the size of the uncompressed entry. The key must also refer to
+the uncompressed entry.
+
 If the `--enable_ac_key_instance_mangling` flag is specified and the instance
 name is not empty, then action cache keys are hashed along with the instance
 name to produce the action cache lookup key. Since the URL path is processed
@@ -93,6 +101,19 @@ which can be enabled with the `--experimental_remote_asset_api` flag.
 To use this with Bazel, specify
 [--experimental_remote_downloader=grpc://replace-with-your.host:port](https://docs.bazel.build/versions/master/command-line-reference.html#flag--experimental_remote_downloader).
 
+### Byte Stream compressed-blobs
+
+This version of bazel-remote supports the
+[Byte Stream compressed-blobs proposal](https://github.com/bazelbuild/remote-apis/pull/168),
+which provides a way for clients to upload and download CAS blobs compressed
+with zstandard, in order to improve network efficiency.
+
+Uploaded CAS blobs are stored in a zstandard compressed format by default,
+which can increase the effective cache size and reduce load on the server
+if clients also download blobs in zstandard compressed form. If you would
+rather store CAS blobs in uncompressed form, add `--storage_mode uncompressed`
+to your configuration.
+
 ## Usage
 
 If a YAML configuration file is specified by the `--config_file` command line
@@ -115,39 +136,44 @@ DESCRIPTION:
    A remote build cache for Bazel.
 
 GLOBAL OPTIONS:
-   --config_file value                Path to a YAML configuration file. If this flag is specified then all other flags are ignored. [$BAZEL_REMOTE_CONFIG_FILE]
-   --dir value                        Directory path where to store the cache contents. This flag is required. [$BAZEL_REMOTE_DIR]
-   --max_size value                   The maximum size of the remote cache in GiB. This flag is required. (default: -1) [$BAZEL_REMOTE_MAX_SIZE]
-   --host value                       Address to listen on. Listens on all network interfaces by default. [$BAZEL_REMOTE_HOST]
-   --port value                       The port the HTTP server listens on. (default: 8080) [$BAZEL_REMOTE_PORT]
-   --grpc_port value                  The port the gRPC server listens on. Set to 0 to disable. (default: 9092) [$BAZEL_REMOTE_GRPC_PORT]
-   --profile_host value               A host address to listen on for profiling, if enabled by a valid --profile_port setting. (default: "127.0.0.1") [$BAZEL_REMOTE_PROFILE_HOST]
-   --profile_port value               If a positive integer, serve /debug/pprof/* URLs from http://profile_host:profile_port. (default: 0, ie profiling disabled) [$BAZEL_REMOTE_PROFILE_PORT]
-   --http_read_timeout value          The HTTP read timeout for a client request in seconds (does not apply to the proxy backends or the profiling endpoint) (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_READ_TIMEOUT]
-   --http_write_timeout value         The HTTP write timeout for a server response in seconds (does not apply to the proxy backends or the profiling endpoint) (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_WRITE_TIMEOUT]
-   --htpasswd_file value              Path to a .htpasswd file. This flag is optional. Please read https://httpd.apache.org/docs/2.4/programs/htpasswd.html. [$BAZEL_REMOTE_HTPASSWD_FILE]
-   --tls_enabled                      This flag has been deprecated. Specify tls_cert_file and tls_key_file instead. (default: false) [$BAZEL_REMOTE_TLS_ENABLED]
-   --tls_ca_file value                Optional. Enables mTLS (authenticating client certificates), should be the certificate authority that signed the client certificates. [$BAZEL_REMOTE_TLS_CA_FILE]
-   --tls_cert_file value              Path to a pem encoded certificate file. [$BAZEL_REMOTE_TLS_CERT_FILE]
-   --tls_key_file value               Path to a pem encoded key file. [$BAZEL_REMOTE_TLS_KEY_FILE]
-   --idle_timeout value               The maximum period of having received no request after which the server will shut itself down. (default: 0s, ie disabled) [$BAZEL_REMOTE_IDLE_TIMEOUT]
-   --max_queued_uploads value         When using proxy backends, sets the maximum number of objects in queue for upload. If the queue is full, uploads will be skipped until the queue has space again. (default: 1000000) [$BAZEL_REMOTE_MAX_QUEUED_UPLOADS]
-   --num_uploaders value              When using proxy backends, sets the number of Goroutines to process parallel uploads to backend. (default: 100) [$BAZEL_REMOTE_NUM_UPLOADERS]
-   --s3.endpoint value                The S3/minio endpoint to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_ENDPOINT]
-   --s3.bucket value                  The S3/minio bucket to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_BUCKET]
-   --s3.prefix value                  The S3/minio object prefix to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_PREFIX]
-   --s3.access_key_id value           The S3/minio access key to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_ACCESS_KEY_ID]
-   --s3.secret_access_key value       The S3/minio secret access key to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_SECRET_ACCESS_KEY]
-   --s3.disable_ssl                   Whether to disable TLS/SSL when using the S3 proxy backend. (default: false, ie enable TLS/SSL) [$BAZEL_REMOTE_S3_DISABLE_SSL]
-   --s3.iam_role_endpoint value       Endpoint for using IAM security credentials. By default it will look for credentials in the standard locations for the AWS platform. [$BAZEL_REMOTE_S3_IAM_ROLE_ENDPOINT]
-   --s3.region value                  The AWS region. Required when not specifying S3/minio access keys. [$BAZEL_REMOTE_S3_REGION]
-   --s3.key_version value             Set to 1 for the legacy flat key format, or 2 for the newer format that reduces the impact of S3 rate limits. (default: 1) [$BAZEL_REMOTE_S3_KEY_VERSION]
-   --disable_http_ac_validation       Whether to disable ActionResult validation for HTTP requests. (default: false, ie enable validation) [$BAZEL_REMOTE_DISABLE_HTTP_AC_VALIDATION]
-   --disable_grpc_ac_deps_check       Whether to disable ActionResult dependency checks for gRPC GetActionResult requests. (default: false, ie enable ActionCache dependency checks) [$BAZEL_REMOTE_DISABLE_GRPS_AC_DEPS_CHECK]
-   --enable_ac_key_instance_mangling  Whether to enable mangling ActionCache keys with non-empty instance names. (default: false, ie disable mangling) [$BAZEL_REMOTE_ENABLE_AC_KEY_INSTANCE_MANGLING]
-   --enable_endpoint_metrics          Whether to enable metrics for each HTTP/gRPC endpoint. (default: false, ie disable metrics) [$BAZEL_REMOTE_ENABLE_ENDPOINT_METRICS]
-   --experimental_remote_asset_api    Whether to enable the experimental remote asset API implementation. (default: false, ie disable remote asset API) [$BAZEL_REMOTE_EXPERIMENTAL_REMOTE_ASSET_API]
-   --help, -h                         show help (default: false)
+   --config_file value                      Path to a YAML configuration file. If this flag is specified then all other flags are ignored. [$BAZEL_REMOTE_CONFIG_FILE]
+   --dir value                              Directory path where to store the cache contents. This flag is required. [$BAZEL_REMOTE_DIR]
+   --max_size value                         The maximum size of the remote cache in GiB. This flag is required. (default: -1) [$BAZEL_REMOTE_MAX_SIZE]
+   --storage_mode value                     Which format to store CAS blobs in. Must be one of "zstd" or "uncompressed". (default: "zstd") [$BAZEL_REMOTE_STORAGE_MODE]
+   --host value                             Address to listen on. Listens on all network interfaces by default. [$BAZEL_REMOTE_HOST]
+   --port value                             The port the HTTP server listens on. (default: 8080) [$BAZEL_REMOTE_PORT]
+   --grpc_port value                        The port the gRPC server listens on. Set to 0 to disable. (default: 9092) [$BAZEL_REMOTE_GRPC_PORT]
+   --profile_host value                     A host address to listen on for profiling, if enabled by a valid --profile_port setting. (default: "127.0.0.1") [$BAZEL_REMOTE_PROFILE_HOST]
+   --profile_port value                     If a positive integer, serve /debug/pprof/* URLs from http://profile_host:profile_port. (default: 0, ie profiling disabled) [$BAZEL_REMOTE_PROFILE_PORT]
+   --http_read_timeout value                The HTTP read timeout for a client request in seconds (does not apply to the proxy backends or the profiling endpoint) (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_READ_TIMEOUT]
+   --http_write_timeout value               The HTTP write timeout for a server response in seconds (does not apply to the proxy backends or the profiling endpoint) (default: 0s, ie disabled) [$BAZEL_REMOTE_HTTP_WRITE_TIMEOUT]
+   --htpasswd_file value                    Path to a .htpasswd file. This flag is optional. Please read https://httpd.apache.org/docs/2.4/programs/htpasswd.html. [$BAZEL_REMOTE_HTPASSWD_FILE]
+   --tls_enabled                            This flag has been deprecated. Specify tls_cert_file and tls_key_file instead. (default: false) [$BAZEL_REMOTE_TLS_ENABLED]
+   --tls_ca_file value                      Optional. Enables mTLS (authenticating client certificates), should be the certificate authority that signed the client certificates. [$BAZEL_REMOTE_TLS_CA_FILE]
+   --tls_cert_file value                    Path to a pem encoded certificate file. [$BAZEL_REMOTE_TLS_CERT_FILE]
+   --tls_key_file value                     Path to a pem encoded key file. [$BAZEL_REMOTE_TLS_KEY_FILE]
+   --idle_timeout value                     The maximum period of having received no request after which the server will shut itself down. (default: 0s, ie disabled) [$BAZEL_REMOTE_IDLE_TIMEOUT]
+   --max_queued_uploads value               When using proxy backends, sets the maximum number of objects in queue for upload. If the queue is full, uploads will be skipped until the queue has space again. (default: 1000000) [$BAZEL_REMOTE_MAX_QUEUED_UPLOADS]
+   --num_uploaders value                    When using proxy backends, sets the number of Goroutines to process parallel uploads to backend. (default: 100) [$BAZEL_REMOTE_NUM_UPLOADERS]
+   --http_proxy.url value                   The base URL to use for a http proxy backend. [$BAZEL_REMOTE_HTTP_PROXY_URL]
+   --gcs_proxy.bucket value                 The bucket to use for the Google Cloud Storage proxy backend. [$BAZEL_REMOTE_GCS_BUCKET]
+   --gcs_proxy.use_default_credentials      Whether or not to use authentication for the Google Cloud Storage proxy backend. (default: false) [$BAZEL_REMOTE_GCS_USE_DEFAULT_CREDENTIALS]
+   --gcs_proxy.json_credentials_file value  Path to a JSON file that contains Google credentials for the Google Cloud Storage proxy backend. [$BAZEL_REMOTE_GCS_JSON_CREDENTIALS_FILE]
+   --s3.endpoint value                      The S3/minio endpoint to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_ENDPOINT]
+   --s3.bucket value                        The S3/minio bucket to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_BUCKET]
+   --s3.prefix value                        The S3/minio object prefix to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_PREFIX]
+   --s3.access_key_id value                 The S3/minio access key to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_ACCESS_KEY_ID]
+   --s3.secret_access_key value             The S3/minio secret access key to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_SECRET_ACCESS_KEY]
+   --s3.disable_ssl                         Whether to disable TLS/SSL when using the S3 proxy backend. (default: false, ie enable TLS/SSL) [$BAZEL_REMOTE_S3_DISABLE_SSL]
+   --s3.iam_role_endpoint value             Endpoint for using IAM security credentials. By default it will look for credentials in the standard locations for the AWS platform. [$BAZEL_REMOTE_S3_IAM_ROLE_ENDPOINT]
+   --s3.region value                        The AWS region. Required when not specifying S3/minio access keys. [$BAZEL_REMOTE_S3_REGION]
+   --s3.key_version value                   Set to 1 for the legacy flat key format, or 2 for the newer format that reduces the impact of S3 rate limits. (default: 1) [$BAZEL_REMOTE_S3_KEY_VERSION]
+   --disable_http_ac_validation             Whether to disable ActionResult validation for HTTP requests. (default: false, ie enable validation) [$BAZEL_REMOTE_DISABLE_HTTP_AC_VALIDATION]
+   --disable_grpc_ac_deps_check             Whether to disable ActionResult dependency checks for gRPC GetActionResult requests. (default: false, ie enable ActionCache dependency checks) [$BAZEL_REMOTE_DISABLE_GRPS_AC_DEPS_CHECK]
+   --enable_ac_key_instance_mangling        Whether to enable mangling ActionCache keys with non-empty instance names. (default: false, ie disable mangling) [$BAZEL_REMOTE_ENABLE_AC_KEY_INSTANCE_MANGLING]
+   --enable_endpoint_metrics                Whether to enable metrics for each HTTP/gRPC endpoint. (default: false, ie disable metrics) [$BAZEL_REMOTE_ENABLE_ENDPOINT_METRICS]
+   --experimental_remote_asset_api          Whether to enable the experimental remote asset API implementation. (default: false, ie disable remote asset API) [$BAZEL_REMOTE_EXPERIMENTAL_REMOTE_ASSET_API]
+   --help, -h                               show help (default: false)
 ```
 
 ### Example configuration file
@@ -157,6 +183,9 @@ GLOBAL OPTIONS:
 dir: path/to/cache-dir
 max_size: 100
 
+# The form to store CAS blobs in ("zstd" or "uncompressed"):
+#storage_mode: zstd
+
 host: localhost
 # The port to use for HTTP/HTTPS:
 #port: 8080
diff --git a/WORKSPACE b/WORKSPACE
index fe4a4d6..00c79cc 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -969,15 +969,15 @@ go_repository(
 go_repository(
     name = "com_github_klauspost_compress",
     importpath = "github.com/klauspost/compress",
-    sum = "h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc=",
-    version = "v1.11.3",
+    sum = "h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=",
+    version = "v1.11.4",
 )
 
 go_repository(
     name = "com_github_mostynb_go_grpc_compression",
     importpath = "github.com/mostynb/go-grpc-compression",
-    sum = "h1:emrImHjSPW/H4aarNxuiPG1uN1WgIlVdR3M0LH5HH5E=",
-    version = "v1.1.2",
+    sum = "h1:wlscKqxoQsZQxUi6uTER+Gh9MbcUFUa4Cw5A69pj/c8=",
+    version = "v1.1.4",
 )
 
 go_repository(
@@ -1070,3 +1070,10 @@ go_repository(
     sum = "h1:y7Vn4YH/rfUHOCwNhvkAcA0gMQvFdKzSE8Ri3qtcFlc=",
     version = "v1.10.1",
 )
+
+go_repository(
+    name = "com_github_mostynb_zstdpool_syncpool",
+    importpath = "github.com/mostynb/zstdpool-syncpool",
+    sum = "h1:tSmtZSiSAfffEgy9ziKvOZOOlccUwF2Ar78+CeOH+8A=",
+    version = "v0.0.2",
+)
diff --git a/cache/cache.go b/cache/cache.go
index 49c7cc9..7c5281e 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -4,7 +4,6 @@ import (
 	"crypto/sha256"
 	"encoding/hex"
 	"io"
-	"path/filepath"
 )
 
 // EntryKind describes the kind of cache entry
@@ -33,6 +32,16 @@ func (e EntryKind) String() string {
 	return "raw"
 }
 
+func (e EntryKind) DirName() string {
+	if e == AC {
+		return "ac.v2"
+	}
+	if e == CAS {
+		return "cas.v2"
+	}
+	return "raw.v2"
+}
+
 // Logger is designed to be satisfied by log.Logger.
 type Logger interface {
 	Printf(format string, v ...interface{})
@@ -53,14 +62,20 @@ func (e *Error) Error() string {
 // Proxy is the interface that (optional) proxy backends must implement.
 // Implementations are expected to be safe for concurrent use.
 type Proxy interface {
-	// Put should make a reasonable effort to proxy this data to the backend.
-	// This is allowed to fail silently (eg when under heavy load).
-	Put(kind EntryKind, hash string, size int64, rdr io.ReadCloser)
 
-	// Get should return the cache item identified by `hash`, or an error
-	// if something went wrong. If the item was not found, the io.ReadCloser
-	// will be nil.
-	Get(kind EntryKind, hash string) (io.ReadCloser, int64, error)
+	// Put makes a reasonable effort to upload the cache item identified by
+	// `hash` with logical size `size`, whose data is readable from `rc` to
+	// the proxy backend. The data available in `rc` is in the same format
+	// as used by the disk.Cache instance.
+	//
+	// This is allowed to fail silently (for example when under heavy load).
+	Put(kind EntryKind, hash string, size int64, rc io.ReadCloser)
+
+	// Get returns an io.ReadCloser from which the cache item identified by
+	// `hash` can be read, its logical size, and an error if something went
+	// wrong. The data available from `rc` is in the same format as used by
+	// the disk.Cache instance.
+	Get(kind EntryKind, hash string) (rc io.ReadCloser, size int64, err error)
 
 	// Contains returns whether or not the cache item exists on the
 	// remote end, and the size if it exists (and -1 if the size is
@@ -87,7 +102,6 @@ func TransformActionCacheKey(key, instance string, logger Logger) string {
 	return newKey
 }
 
-// Key returns the proper cache key for an entry kind and hash.
-func Key(kind EntryKind, hash string) string {
-	return filepath.Join(kind.String(), hash[:2], hash)
+func LookupKey(kind EntryKind, hash string) string {
+	return kind.String() + "/" + hash
 }
diff --git a/cache/disk/BUILD.bazel b/cache/disk/BUILD.bazel
index cb2e706..32e6465 100644
--- a/cache/disk/BUILD.bazel
+++ b/cache/disk/BUILD.bazel
@@ -10,6 +10,7 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//cache:go_default_library",
+        "//cache/disk/casblob:go_default_library",
         "//genproto/build/bazel/remote/execution/v2:go_default_library",
         "//utils/tempfile:go_default_library",
         "@com_github_djherbis_atime//:go_default_library",
@@ -28,6 +29,7 @@ go_test(
     embed = [":go_default_library"],
     deps = [
         "//cache:go_default_library",
+        "//cache/disk/casblob:go_default_library",
         "//cache/httpproxy:go_default_library",
         "//genproto/build/bazel/remote/execution/v2:go_default_library",
         "//utils:go_default_library",
diff --git a/cache/disk/casblob/BUILD.bazel b/cache/disk/casblob/BUILD.bazel
new file mode 100644
index 0000000..8044666
--- /dev/null
+++ b/cache/disk/casblob/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["casblob.go"],
+    importpath = "github.com/buchgr/bazel-remote/cache/disk/casblob",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//utils/zstdpool:go_default_library",
+        "@com_github_klauspost_compress//zstd:go_default_library",
+        "@com_github_mostynb_zstdpool_syncpool//:go_default_library",
+    ],
+)
+
+go_test(
+    name = "go_default_test",
+    srcs = ["casblob_test.go"],
+    embed = [":go_default_library"],
+)
diff --git a/cache/disk/casblob/casblob.go b/cache/disk/casblob/casblob.go
new file mode 100644
index 0000000..edd09bb
--- /dev/null
+++ b/cache/disk/casblob/casblob.go
@@ -0,0 +1,626 @@
+package casblob
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/binary"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+
+	"github.com/buchgr/bazel-remote/utils/zstdpool"
+	"github.com/klauspost/compress/zstd"
+	syncpool "github.com/mostynb/zstdpool-syncpool"
+)
+
+type CompressionType uint8
+
+const (
+	Identity  CompressionType = 0
+	Zstandard CompressionType = 1
+)
+
+var zstdFastestLevel = zstd.WithEncoderLevel(zstd.SpeedFastest)
+
+var encoder, _ = zstd.NewWriter(nil, zstdFastestLevel) // TODO: raise WithEncoderConcurrency ?
+var decoder, _ = zstd.NewReader(nil)                   // TODO: raise WithDecoderConcurrency ?
+
+var encoderPool = zstdpool.GetEncoderPool()
+var decoderPool = zstdpool.GetDecoderPool()
+
+var errDecoderPoolFail error = errors.New("failed to get DecoderWrapper from pool")
+var errEncoderPoolFail error = errors.New("failed to get EncoderWrapper from pool")
+
+const defaultChunkSize = 1024 * 1024 * 1 // 1M
+
+// 4 bytes, to be written to disk in little-endian format.
+// https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#skippable-frames
+const skippableFrameMagicNumber = 0x184D2A50
+
+// Compressed CAS blobs are stored in .zst format, with a header which
+// describes the locations of independently compressed chunks.
+type header struct {
+	// The following data is stored in little-endian format on disk.
+
+	// We start with two zstd skippable frame fields:
+
+	//magicNumber uint32 // 4 bytes: skippableFrameMagicNumber
+
+	// With 1M chunks this gives a limit of ~511TB which is plenty.
+	//frameSize   uint32 // 4 bytes: the size of the rest of the header.
+
+	// Then we put our metadata:
+
+	uncompressedSize int64           // 8 bytes
+	compression      CompressionType // uint8, 1 byte
+	chunkSize        uint32          // 4 bytes
+
+	// Offsets in the file on disk, of each chunk, with an additional
+	// entry for the end of the file.
+	//
+	// Stored as an int64 number of chunks, followed by their int64 offsets,
+	// and a final value for the size of the file (header + data).
+	// 8 bytes + (n+1)*8 bytes.
+	chunkOffsets []int64
+}
+
+const chunkTableOffset = 4 + 4 + 8 + 1 + 4 + 8
+
+// Returns the size of the header itself.
+func (h *header) size() int64 {
+	return chunkTableOffset + (int64(len(h.chunkOffsets)) * 8)
+}
+
+func (h *header) frameSize() uint32 {
+	return chunkTableOffset + (uint32(len(h.chunkOffsets)) * 8) - 4 - 4
+}
+
+// Provides an io.ReadCloser that returns uncompressed data from a cas blob.
+type readCloserWrapper struct {
+	*header
+
+	rdr io.Reader // Read from this, not from decoder or file.
+
+	decoder *syncpool.DecoderWrapper // Might be nil.
+	file    *os.File
+}
+
+var errWrongMagicNum = errors.New("expected magic number not found")
+
+// Read the header and leave f at the start of the data.
+func readHeader(f *os.File) (*header, error) {
+	var err error
+	var h header
+
+	fileInfo, err := f.Stat()
+	if err != nil {
+		return nil, err
+	}
+	foundFileSize := fileInfo.Size()
+	if foundFileSize <= (chunkTableOffset + 16) {
+		// The file must have a header with at least two chunkOffsets.
+		return nil, fmt.Errorf("file too small (%d) than the minimum header size (%d)",
+			foundFileSize, (chunkTableOffset + 16))
+	}
+
+	var magicNumber uint32
+	err = binary.Read(f, binary.LittleEndian, &magicNumber)
+	if err != nil {
+		return nil, fmt.Errorf("unable to read magic number: %w", err)
+	}
+	if magicNumber != skippableFrameMagicNumber {
+		return nil, errWrongMagicNum
+	}
+
+	var frameSize uint32
+	err = binary.Read(f, binary.LittleEndian, &frameSize)
+	if err != nil {
+		return nil, fmt.Errorf("unable to read frameSize: %w", err)
+	}
+
+	err = binary.Read(f, binary.LittleEndian, &h.uncompressedSize)
+	if err != nil {
+		return nil, err
+	}
+
+	err = binary.Read(f, binary.LittleEndian, &h.compression)
+	if err != nil {
+		return nil, err
+	}
+
+	err = binary.Read(f, binary.LittleEndian, &h.chunkSize)
+	if err != nil {
+		return nil, err
+	}
+
+	var numOffsets int64
+	err = binary.Read(f, binary.LittleEndian, &numOffsets)
+	if err != nil {
+		return nil, err
+	}
+
+	if numOffsets < 2 {
+		// chunkOffsets has an extra entry to specify the compressed file size.
+		// Note that we never store empty files.
+		return nil, fmt.Errorf("internal error: need at least one chunk, found %d", numOffsets-1)
+	}
+
+	metadataSize := numOffsets*8 + 8 + 1 + 4 + 8
+	if int64(frameSize) != metadataSize {
+		return nil, fmt.Errorf("metadata frame size %d, but metadata size %d",
+			frameSize, metadataSize)
+	}
+
+	h.chunkOffsets = make([]int64, numOffsets, numOffsets)
+	err = binary.Read(f, binary.LittleEndian, h.chunkOffsets)
+	if err != nil {
+		return nil, err
+	}
+
+	prevOffset := int64(-1)
+	for i := 0; int64(i) < numOffsets; i++ {
+		if h.chunkOffsets[i] <= prevOffset {
+			return nil,
+				fmt.Errorf("offset table values should increase: %d -> %d",
+					h.chunkOffsets[i], prevOffset)
+		}
+		prevOffset = h.chunkOffsets[i]
+	}
+
+	if prevOffset != foundFileSize {
+		return nil,
+			fmt.Errorf("final offset in chunk table %d should be file size %d",
+				prevOffset, foundFileSize)
+	}
+
+	return &h, nil
+}
+
+// Extract the logical size of a v2 cas blob from rc, and return that
+// size along with an equivalent io.ReadCloser to rc.
+func ExtractLogicalSize(rc io.ReadCloser) (io.ReadCloser, int64, error) {
+
+	// Read the first part of the header: magic number (4 bytes),
+	// frame size (4 bytes), uncompressed size (8 bytes).
+	interesting := 16
+	earlyHeader := make([]byte, interesting, interesting)
+
+	n, err := io.ReadFull(rc, earlyHeader)
+	if err != nil {
+		return nil, -1, err
+	}
+	if n != 16 {
+		return nil, -1, fmt.Errorf("Tried to read 16 header bytes, only read %d", n)
+	}
+
+	var uncompressedSize int64
+	br := bytes.NewReader(earlyHeader[8:])
+	err = binary.Read(br, binary.LittleEndian, &uncompressedSize)
+	if err != nil {
+		return nil, -1, err
+	}
+	if uncompressedSize <= 0 {
+		return nil, -1, fmt.Errorf("Expected blob to have positive size, found %d",
+			uncompressedSize)
+	}
+
+	return &multiReadCloser{
+		Reader: io.MultiReader(bytes.NewReader(earlyHeader), rc),
+		rc:     rc,
+	}, uncompressedSize, nil
+}
+
+type multiReadCloser struct {
+	io.Reader // This will be a MultiReader.
+	rc        io.ReadCloser
+}
+
+func (m *multiReadCloser) Close() error {
+	return m.rc.Close()
+}
+
+// Returns an io.ReadCloser that provides uncompressed data. The caller
+// must close the returned io.ReadCloser if it is non-nil. Doing so
+// will automatically close f. If there is an error f will be closed, the caller
+// does not need to do so.
+func GetUncompressedReadCloser(f *os.File, expectedSize int64, offset int64) (io.ReadCloser, error) {
+	h, err := readHeader(f)
+	if err != nil {
+		f.Close()
+		return nil, err
+	}
+
+	if expectedSize != -1 && h.uncompressedSize != expectedSize {
+		f.Close()
+		return nil, fmt.Errorf("expected a blob of size %d, found %d",
+			expectedSize, h.uncompressedSize)
+	}
+
+	if h.compression == Identity {
+		// Simple case. Assumes that we only have one chunk if the data is
+		// uncompressed (which makes sense).
+
+		if offset > 0 {
+			_, err = f.Seek(offset, io.SeekCurrent)
+			if err != nil {
+				f.Close()
+				return nil, err
+			}
+		}
+
+		return f, nil
+	}
+
+	if h.compression != Zstandard {
+		f.Close()
+		return nil,
+			fmt.Errorf("internal error: unsupported compression type %d",
+				h.compression)
+	}
+
+	// Find the first relevant chunk.
+	chunkNum := int64(offset / int64(h.chunkSize))
+	remainder := offset % int64(h.chunkSize)
+
+	if chunkNum > 0 {
+		f.Seek(h.chunkOffsets[chunkNum], io.SeekStart)
+	}
+	if remainder == 0 {
+		z, ok := decoderPool.Get().(*syncpool.DecoderWrapper)
+		if !ok {
+			f.Close()
+			return nil, err
+		}
+		err := z.Reset(f)
+		if err != nil {
+			f.Close()
+			return nil, err
+		}
+
+		return &readCloserWrapper{
+			header:  h,
+			rdr:     z,
+			decoder: z,
+			file:    f,
+		}, nil
+	}
+
+	compressedFirstChunk := make([]byte, h.chunkOffsets[chunkNum+1]-h.chunkOffsets[chunkNum])
+	_, err = io.ReadFull(f, compressedFirstChunk)
+	if err != nil {
+		f.Close()
+		return nil, err
+	}
+
+	uncompressedFirstChunk, err := decoder.DecodeAll(compressedFirstChunk, nil)
+	if err != nil {
+		f.Close()
+		return nil, err
+	}
+
+	if chunkNum == int64(len(h.chunkOffsets)-2) {
+		// Last chunk in the file.
+		r := bytes.NewReader(uncompressedFirstChunk[remainder:])
+		f.Close()
+		return ioutil.NopCloser(r), nil
+	}
+
+	z, ok := decoderPool.Get().(*syncpool.DecoderWrapper)
+	if !ok {
+		f.Close()
+		return nil, errDecoderPoolFail
+	}
+
+	err = z.Reset(f)
+	if err != nil {
+		f.Close()
+		decoderPool.Put(z)
+		return nil, fmt.Errorf("Failed to setup zstd decoder: %w", err)
+	}
+
+	br := bytes.NewReader(uncompressedFirstChunk[remainder:])
+
+	return &readCloserWrapper{
+		header:  h,
+		rdr:     io.MultiReader(br, z),
+		decoder: z,
+		file:    f,
+	}, nil
+}
+
+// Returns an io.ReadCloser that provides zstandard compressed data. The
+// caller must close the returned io.ReadCloser if it is non-nil. Doing so
+// will automatically close f. If there is an error f will be closed, the caller
+// does not need to do so.
+func GetZstdReadCloser(f *os.File, expectedSize int64, offset int64) (io.ReadCloser, error) {
+
+	h, err := readHeader(f)
+	if err != nil {
+		f.Close()
+		return nil, err
+	}
+
+	if expectedSize != -1 && h.uncompressedSize != expectedSize {
+		f.Close()
+		return nil, fmt.Errorf("expected a blob of size %d, found %d",
+			expectedSize, h.uncompressedSize)
+	}
+
+	if h.compression == Identity {
+		// Simple case. Assumes that we only have one chunk if the data is
+		// uncompressed (which makes sense).
+
+		if offset > 0 {
+			_, err = f.Seek(offset, io.SeekCurrent)
+			if err != nil {
+				f.Close()
+				return nil, err
+			}
+		}
+
+		return GetLegacyZstdReadCloser(f)
+	}
+
+	if h.compression != Zstandard {
+		f.Close()
+		return nil, fmt.Errorf("unsupported compression type: %d",
+			h.compression)
+	}
+
+	// Find the first relevant chunk.
+	chunkNum := int64(offset / int64(h.chunkSize))
+	remainder := offset % int64(h.chunkSize)
+
+	if chunkNum > 0 {
+		f.Seek(h.chunkOffsets[chunkNum], io.SeekStart)
+	}
+
+	if remainder == 0 {
+		// Simple case- just stream the file from here.
+		return f, nil
+	}
+
+	compressedFirstChunk := make([]byte, h.chunkOffsets[chunkNum+1]-h.chunkOffsets[chunkNum])
+	_, err = io.ReadFull(f, compressedFirstChunk)
+	if err != nil {
+		f.Close()
+		return nil, err
+	}
+
+	uncompressedFirstChunk, err := decoder.DecodeAll(compressedFirstChunk, nil)
+	if err != nil {
+		f.Close()
+		return nil, err
+	}
+
+	chunkToRecompress := uncompressedFirstChunk[remainder:]
+	recompressedChunk := encoder.EncodeAll(chunkToRecompress, nil)
+
+	br := bytes.NewReader(recompressedChunk)
+	if chunkNum == int64(len(h.chunkOffsets)-2) {
+		f.Close()
+		return ioutil.NopCloser(br), nil
+	}
+
+	return &readCloserWrapper{
+		header: h,
+		rdr:    io.MultiReader(br, f),
+		file:   f,
+	}, nil
+}
+
+// GetLegacyZstdReadCloser returns an io.ReadCloser that provides
+// zstandard-compressed data from an uncompressed file.
+func GetLegacyZstdReadCloser(f *os.File) (io.ReadCloser, error) {
+	enc, ok := encoderPool.Get().(*syncpool.EncoderWrapper)
+	if !ok {
+		f.Close()
+		return nil, errEncoderPoolFail
+	}
+
+	pr, pw := io.Pipe()
+	enc.Reset(pw)
+
+	go func() {
+		// Read from the file, write to enc.
+
+		// TODO: consider implementing something with a timeout?
+		_, err := enc.ReadFrom(f)
+		if err != nil {
+			log.Println("Error while reading/compressing file:", err)
+
+			// Reading from pr will now receive this error:
+			pw.CloseWithError(err)
+		}
+
+		encoderPool.Put(enc)
+		f.Close()
+	}()
+
+	return pr, nil
+}
+
+func (h *header) write(f *os.File) error {
+	var err error
+
+	err = binary.Write(f, binary.LittleEndian, uint32(skippableFrameMagicNumber))
+	if err != nil {
+		return err
+	}
+
+	err = binary.Write(f, binary.LittleEndian, h.frameSize())
+	if err != nil {
+		return err
+	}
+
+	err = binary.Write(f, binary.LittleEndian, h.uncompressedSize)
+	if err != nil {
+		return err
+	}
+
+	err = binary.Write(f, binary.LittleEndian, h.compression)
+	if err != nil {
+		return err
+	}
+
+	err = binary.Write(f, binary.LittleEndian, h.chunkSize)
+	if err != nil {
+		return err
+	}
+
+	err = binary.Write(f, binary.LittleEndian, int64(len(h.chunkOffsets)))
+	if err != nil {
+		return err
+	}
+
+	return binary.Write(f, binary.LittleEndian, h.chunkOffsets)
+}
+
+func (b *readCloserWrapper) Read(p []byte) (int, error) {
+	return b.rdr.Read(p)
+}
+
+func (b *readCloserWrapper) Close() error {
+	if b.decoder != nil {
+		b.decoder.Close()
+		b.decoder = nil
+	}
+
+	if b.file == nil {
+		return nil
+	}
+
+	f := b.file
+	b.file = nil
+
+	return f.Close()
+}
+
+// Read from r and write to f, using CompressionType t.
+// Return the size on disk or an error if something went wrong.
+func WriteAndClose(r io.Reader, f *os.File, t CompressionType, hash string, size int64) (int64, error) {
+	var err error
+	defer f.Close()
+
+	if size <= 0 {
+		return -1, fmt.Errorf("invalid file size: %d", size)
+	}
+
+	chunkSize := uint32(defaultChunkSize)
+
+	numChunks := int64(1)
+	remainder := int64(0)
+	if t == Zstandard {
+		numChunks = size / int64(chunkSize)
+		remainder = size % int64(chunkSize)
+		if remainder > 0 {
+			numChunks++
+		}
+	}
+
+	numOffsets := numChunks + 1
+	h := header{
+		uncompressedSize: size,
+		compression:      t,
+		chunkSize:        chunkSize,
+		chunkOffsets:     make([]int64, numOffsets, numOffsets),
+	}
+
+	h.chunkOffsets[0] = chunkTableOffset
+
+	err = h.write(f)
+	if err != nil {
+		return -1, err
+	}
+
+	fileOffset := h.size()
+
+	var n int64
+
+	if t == Identity {
+		hasher := sha256.New()
+
+		n, err = io.Copy(io.MultiWriter(f, hasher), r)
+		if err != nil {
+			return -1, err
+		}
+		if n != size {
+			return -1, fmt.Errorf("expected to copy %d bytes, actually copied %d bytes",
+				size, n)
+		}
+
+		actualHash := hex.EncodeToString(hasher.Sum(nil))
+		if actualHash != hash {
+			return -1,
+				fmt.Errorf("checksums don't match. Expected %s, found %s",
+					hash, actualHash)
+		}
+
+		return n + fileOffset, f.Close()
+	}
+
+	// Compress the data in chunks...
+
+	nextChunk := 0 // Index in h.chunkOffsets.
+	remainingRawData := size
+
+	uncompressedChunk := make([]byte, chunkSize, chunkSize)
+
+	hasher := sha256.New()
+
+	for nextChunk < len(h.chunkOffsets)-1 {
+		h.chunkOffsets[nextChunk] = fileOffset
+		nextChunk++
+
+		chunkEnd := int64(chunkSize)
+		if remainingRawData <= int64(chunkSize) {
+			chunkEnd = remainingRawData
+		}
+		remainingRawData -= chunkEnd
+
+		_, err = io.ReadFull(r, uncompressedChunk[0:chunkEnd])
+		if err != nil {
+			return -1, err
+		}
+
+		compressedChunk := encoder.EncodeAll(uncompressedChunk[0:chunkEnd], nil)
+
+		hasher.Write(uncompressedChunk[0:chunkEnd])
+
+		written, err := f.Write(compressedChunk)
+		if err != nil {
+			return -1, err
+		}
+
+		fileOffset += int64(written)
+	}
+	h.chunkOffsets[nextChunk] = fileOffset
+
+	actualHash := hex.EncodeToString(hasher.Sum(nil))
+	if actualHash != hash {
+		return -1, fmt.Errorf("checksums don't match. Expected %s, found %s",
+			hash, actualHash)
+	}
+
+	// We know all the chunk offsets now, go back and fill those in.
+	_, err = f.Seek(chunkTableOffset, io.SeekStart)
+	if err != nil {
+		return -1, err
+	}
+
+	err = binary.Write(f, binary.LittleEndian, h.chunkOffsets)
+	if err != nil {
+		return -1, err
+	}
+
+	err = f.Sync()
+	if err != nil {
+		return -1, err
+	}
+
+	return fileOffset, f.Close()
+}
diff --git a/cache/disk/disk.go b/cache/disk/disk.go
index 33390d6..51ef149 100644
--- a/cache/disk/disk.go
+++ b/cache/disk/disk.go
@@ -3,20 +3,24 @@ package disk
 import (
 	"bytes"
 	"crypto/sha256"
-	"encoding/hex"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
 	"net/http"
 	"os"
+	"path"
 	"path/filepath"
 	"regexp"
+	"runtime"
 	"sort"
+	"strconv"
+	"strings"
 	"sync"
-	"syscall"
 
 	"github.com/buchgr/bazel-remote/cache"
+	"github.com/buchgr/bazel-remote/cache/disk/casblob"
 	"github.com/buchgr/bazel-remote/utils/tempfile"
 
 	"github.com/djherbis/atime"
@@ -40,15 +44,31 @@ var (
 
 var tfc = tempfile.NewCreator()
 
+var emptyZstdBlob = []byte{40, 181, 47, 253, 32, 0, 1, 0, 0}
+
+var hashKeyRegex = regexp.MustCompile("^[a-f0-9]{64}$")
+
 // lruItem is the type of the values stored in SizedLRU to keep track of items.
 type lruItem struct {
+	// Size of the blob in uncompressed form.
 	size int64
+
+	// Size of the blob on disk (possibly with header + compression).
+	sizeOnDisk int64
+
+	// A random string (of digits, for now) that is included in the filename.
+	random string
+
+	// If true, the blob is a raw CAS file (no header, uncompressed)
+	// with a ".v1" filename suffix.
+	legacy bool
 }
 
 // Cache is a filesystem-based LRU cache, with an optional backend proxy.
 type Cache struct {
-	dir   string
-	proxy cache.Proxy
+	dir         string
+	proxy       cache.Proxy
+	storageMode casblob.CompressionType
 
 	mu  sync.Mutex
 	lru SizedLRU
@@ -65,44 +85,61 @@ const emptySha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b785
 // New returns a new instance of a filesystem-based cache rooted at `dir`,
 // with a maximum size of `maxSizeBytes` bytes and an optional backend `proxy`.
 // Cache is safe for concurrent use.
-func New(dir string, maxSizeBytes int64, proxy cache.Proxy) (*Cache, error) {
+func New(dir string, maxSizeBytes int64, storageMode string, proxy cache.Proxy) (*Cache, error) {
 	// Create the directory structure.
 	hexLetters := []byte("0123456789abcdef")
 	for _, c1 := range hexLetters {
 		for _, c2 := range hexLetters {
 			subDir := string(c1) + string(c2)
-			err := os.MkdirAll(filepath.Join(dir, cache.CAS.String(), subDir), os.ModePerm)
+			err := os.MkdirAll(filepath.Join(dir, cache.CAS.DirName(), subDir), os.ModePerm)
 			if err != nil {
 				return nil, err
 			}
-			err = os.MkdirAll(filepath.Join(dir, cache.AC.String(), subDir), os.ModePerm)
+			err = os.MkdirAll(filepath.Join(dir, cache.AC.DirName(), subDir), os.ModePerm)
 			if err != nil {
 				return nil, err
 			}
-			err = os.MkdirAll(filepath.Join(dir, cache.RAW.String(), subDir), os.ModePerm)
+			err = os.MkdirAll(filepath.Join(dir, cache.RAW.DirName(), subDir), os.ModePerm)
 			if err != nil {
 				return nil, err
 			}
 		}
 	}
 
+	compressionType := casblob.Zstandard
+	if storageMode == "uncompressed" {
+		compressionType = casblob.Identity
+	}
+
+	c := &Cache{
+		dir:         filepath.Clean(dir),
+		storageMode: compressionType,
+		proxy:       proxy,
+	}
+
 	// The eviction callback deletes the file from disk.
 	// This function is only called while the lock is held
 	// by the current goroutine.
 	onEvict := func(key Key, value lruItem) {
-		f := filepath.Join(dir, key.(string))
-		err := os.Remove(f)
-		if err != nil {
-			log.Printf("ERROR: failed to remove evicted cache file: %s", f)
+		ks := key.(string)
+		hash := ks[len(ks)-sha256.Size*2:]
+		var kind cache.EntryKind = cache.AC
+		if strings.HasPrefix(ks, "cas") {
+			kind = cache.CAS
+		} else if strings.HasPrefix(ks, "ac") {
+			kind = cache.AC
+		} else if strings.HasPrefix(ks, "raw") {
+			kind = cache.RAW
 		}
-	}
 
-	c := &Cache{
-		dir:   filepath.Clean(dir),
-		proxy: proxy,
-		lru:   NewSizedLRU(maxSizeBytes, onEvict),
+		f := filepath.Join(dir, c.FileLocation(kind, value.legacy, hash, value.size, value.random))
+
+		// Run in a goroutine so we can release the lock sooner.
+		go removeFile(f)
 	}
 
+	c.lru = NewSizedLRU(maxSizeBytes, onEvict)
+
 	err := c.migrateDirectories()
 	if err != nil {
 		return nil, fmt.Errorf("Attempting to migrate the old directory structure failed: %w", err)
@@ -115,73 +152,259 @@ func New(dir string, maxSizeBytes int64, proxy cache.Proxy) (*Cache, error) {
 	return c, nil
 }
 
+func removeFile(f string) {
+	err := os.Remove(f)
+	if err != nil {
+		log.Printf("ERROR: failed to remove evicted cache file: %s", f)
+	}
+}
+
+func (c *Cache) FileLocationBase(kind cache.EntryKind, legacy bool, hash string, size int64) string {
+	if kind == cache.RAW {
+		return path.Join("raw.v2", hash[:2], hash)
+	}
+
+	if kind == cache.AC {
+		return path.Join("ac.v2", hash[:2], hash)
+	}
+
+	if legacy {
+		return path.Join("cas.v2", hash[:2], hash)
+	}
+
+	return fmt.Sprintf("cas.v2/%s/%s-%d", hash[:2], hash, size)
+}
+
+func (c *Cache) FileLocation(kind cache.EntryKind, legacy bool, hash string, size int64, random string) string {
+	if kind == cache.RAW {
+		return path.Join("raw.v2", hash[:2], hash+"-"+random)
+	}
+
+	if kind == cache.AC {
+		return path.Join("ac.v2", hash[:2], hash+"-"+random)
+	}
+
+	if legacy {
+		return fmt.Sprintf("cas.v2/%s/%s-%s.v1", hash[:2], hash, random)
+	}
+
+	return fmt.Sprintf("cas.v2/%s/%s-%d-%s", hash[:2], hash, size, random)
+}
+
 func (c *Cache) migrateDirectories() error {
-	err := migrateDirectory(filepath.Join(c.dir, cache.AC.String()))
+	err := migrateDirectory(c.dir, cache.AC)
+	if err != nil {
+		return err
+	}
+	err = migrateDirectory(c.dir, cache.CAS)
 	if err != nil {
 		return err
 	}
-	err = migrateDirectory(filepath.Join(c.dir, cache.CAS.String()))
+	err = migrateDirectory(c.dir, cache.RAW)
 	if err != nil {
 		return err
 	}
-	// Note: there are no old "RAW" directories (yet).
 	return nil
 }
 
-func migrateDirectory(dir string) error {
-	log.Printf("Migrating files (if any) to new directory structure: %s\n", dir)
+func migrateDirectory(baseDir string, kind cache.EntryKind) error {
+	sourceDir := path.Join(baseDir, kind.String())
+
+	_, err := os.Stat(sourceDir)
+	if os.IsNotExist(err) {
+		return nil
+	}
+
+	log.Println("Migrating files (if any) to new directory structure:", sourceDir)
 
-	listing, err := ioutil.ReadDir(dir)
+	listing, err := ioutil.ReadDir(sourceDir)
 	if err != nil {
 		return err
 	}
 
 	// The v0 directory structure was lowercase sha256 hash filenames
 	// stored directly in the ac/ and cas/ directories.
-	hashKeyRegex := regexp.MustCompile("^[a-f0-9]{64}$")
 
 	// The v1 directory structure has subdirs for each two lowercase
 	// hex character pairs.
 	v1DirRegex := regexp.MustCompile("^[a-f0-9]{2}$")
 
-	for _, item := range listing {
-		oldName := item.Name()
-		oldNamePath := filepath.Join(dir, oldName)
+	targetDir := path.Join(baseDir, kind.DirName())
+
+	itemChan := make(chan os.FileInfo)
+	errChan := make(chan error)
+
+	var wg sync.WaitGroup
+	for i := 0; i < runtime.NumCPU(); i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+
+			for item := range itemChan {
+
+				oldName := item.Name()
+				oldNamePath := filepath.Join(sourceDir, oldName)
+
+				if item.IsDir() {
+					if !v1DirRegex.MatchString(oldName) {
+						// Warn about non-v1 subdirectories.
+						log.Println("Warning: unexpected directory", oldNamePath)
+					}
 
-		if item.IsDir() {
-			if !v1DirRegex.MatchString(oldName) {
-				// Warn about non-v1 subdirectories.
-				log.Println("Warning: unexpected directory", oldNamePath)
+					destDir := filepath.Join(targetDir, oldName[:2])
+					err := migrateV1Subdir(oldNamePath, destDir, kind)
+					if err != nil {
+						log.Printf("Warning: failed to read subdir %q: %s",
+							oldNamePath, err)
+						continue
+					}
+
+					continue
+				}
+
+				if !item.Mode().IsRegular() {
+					log.Println("Warning: skipping non-regular file:", oldNamePath)
+					continue
+				}
+
+				if !hashKeyRegex.MatchString(oldName) {
+					log.Println("Warning: skipping unexpected file:", oldNamePath)
+					continue
+				}
+
+				src := filepath.Join(sourceDir, oldName)
+
+				// Add a not-so-random "random" string. This should be OK
+				// since there is probably only be one file for this hash.
+				dest := filepath.Join(targetDir, oldName[:2], oldName+"-222444666")
+				if kind == cache.CAS {
+					dest += ".v1"
+				}
+
+				// TODO: make this work across filesystems?
+				err := os.Rename(src, dest)
+				if err != nil {
+					errChan <- err
+					return
+				}
 			}
-			continue
+		}()
+	}
+
+	err = nil
+	numItems := len(listing)
+	i := 1
+	for _, item := range listing {
+		select {
+		case itemChan <- item:
+			log.Printf("Migrating %s item(s) %d/%d, %s\n", sourceDir, i, numItems, item.Name())
+			i++
+		case err = <-errChan:
+			log.Println("Encountered error while migrating files:", err)
+			close(itemChan)
 		}
+	}
+	close(itemChan)
+	wg.Wait()
+
+	return err
+}
 
-		if !item.Mode().IsRegular() {
-			log.Println("Warning: skipping non-regular file:", oldNamePath)
-			continue
+func migrateV1Subdir(oldDir string, destDir string, kind cache.EntryKind) error {
+	listing, err := ioutil.ReadDir(oldDir)
+	if err != nil {
+		return err
+	}
+
+	if kind == cache.CAS {
+		for _, item := range listing {
+
+			oldPath := path.Join(oldDir, item.Name())
+
+			if !hashKeyRegex.MatchString(item.Name()) {
+				return fmt.Errorf("Unexpected file: %s", oldPath)
+			}
+
+			destPath := path.Join(destDir, item.Name()) + ".v1"
+			err = os.Rename(oldPath, destPath)
+			if err != nil {
+				return fmt.Errorf("Failed to migrate CAS blob %s: %w",
+					oldPath, err)
+			}
 		}
 
-		if !hashKeyRegex.MatchString(oldName) {
-			log.Println("Warning: skipping unexpected file:", oldNamePath)
-			continue
+		return os.Remove(oldDir)
+	}
+
+	var hashKeyRegexWithRandom = regexp.MustCompile("^([a-f0-9]{64})-([0-9]+)$")
+
+	for _, item := range listing {
+		oldPath := path.Join(oldDir, item.Name())
+
+		if !hashKeyRegexWithRandom.MatchString(item.Name()) {
+			return fmt.Errorf("Unexpected file: %s", oldPath)
 		}
 
-		newName := filepath.Join(dir, oldName[:2], oldName)
-		err = os.Rename(filepath.Join(dir, oldName), newName)
+		destPath := path.Join(destDir, item.Name())
+
+		// TODO: support cross-filesystem migration.
+		err = os.Rename(oldPath, destPath)
 		if err != nil {
-			return err
+			return fmt.Errorf("Failed to migrate blob %s: %w", oldPath, err)
 		}
 	}
 
 	return nil
 }
 
+/* TODO: remove this dead code?
+func migrateCASFile(src string, dest string, hash string) error {
+
+	srcFile, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer srcFile.Close()
+
+	fi, err := srcFile.Stat()
+	if err != nil {
+		return err
+	}
+
+	origSize := fi.Size()
+	accessTime := atime.Get(fi)
+
+	destFile, err := os.Create(dest)
+	if err != nil {
+		return err
+	}
+	_, err = casblob.WriteAndClose(srcFile, destFile,
+		casblob.Zstandard, hash, origSize)
+	if err != nil {
+		return err
+	}
+	os.Chtimes(dest, accessTime, fi.ModTime()) // Best effort, ignore errors.
+
+	err = os.Remove(src)
+	if err != nil {
+		return fmt.Errorf("Failed to remove %s: %w", src, err)
+	}
+
+	return err
+}
+*/
+
 // loadExistingFiles lists all files in the cache directory, and adds them to the
 // LRU index so that they can be served. Files are sorted by access time first,
 // so that the eviction behavior is preserved across server restarts.
 func (c *Cache) loadExistingFiles() error {
 	log.Printf("Loading existing files in %s.\n", c.dir)
 
+	// compressed CAS items: <hash>-<logical size>-<random digits/ascii letters>
+	// uncompressed CAS items: <hash>-<logical size>-<random digits/ascii letters>.v1
+	// AC and RAW items: <hash>-<random digits/ascii letters>
+	re := regexp.MustCompile("^([a-f0-9]{64})(?:-([1-9][0-9]*))?-([0-9a-zA-Z]+)(\\.v1)?$")
+
 	// Walk the directory tree
 	var files []nameAndInfo
 	err := filepath.Walk(c.dir, func(name string, info os.FileInfo, err error) error {
@@ -190,9 +413,17 @@ func (c *Cache) loadExistingFiles() error {
 			return err
 		}
 
-		if !info.IsDir() {
+		if info.IsDir() {
+			return nil
+		}
+
+		if info.Mode()&os.ModeSetgid == os.ModeSetgid {
+			log.Println("Removing incomplete file:", name)
+			os.Remove(name)
+		} else {
 			files = append(files, nameAndInfo{name: name, info: info})
 		}
+
 		return nil
 	})
 	if err != nil {
@@ -208,7 +439,50 @@ func (c *Cache) loadExistingFiles() error {
 	log.Println("Building LRU index.")
 	for _, f := range files {
 		relPath := f.name[len(c.dir)+1:]
-		ok := c.lru.Add(relPath, lruItem{size: f.info.Size()})
+
+		fields := strings.Split(relPath, "/")
+
+		file := fields[len(fields)-1]
+
+		sm := re.FindStringSubmatch(file)
+
+		if len(sm) != 5 {
+			return fmt.Errorf("Unrecognized file: %q", relPath)
+		}
+
+		hash := sm[1]
+
+		sizeOnDisk := f.info.Size()
+		size := sizeOnDisk
+		if len(sm[2]) > 0 {
+			size, err = strconv.ParseInt(sm[2], 10, 64)
+		}
+
+		random := sm[3]
+		if len(random) == 0 {
+			return fmt.Errorf("Unrecognized file (no random string): %q", file)
+		}
+
+		legacy := sm[4] == ".v1"
+
+		var lookupKey string
+
+		if strings.HasPrefix(relPath, "cas.v2/") {
+			lookupKey = "cas/" + hash
+		} else if strings.HasPrefix(relPath, "ac.v2/") {
+			lookupKey = "ac/" + hash
+		} else if strings.HasPrefix(relPath, "raw.v2/") {
+			lookupKey = "raw/" + hash
+		} else {
+			return fmt.Errorf("Unrecognised file in cache dir: %q", relPath)
+		}
+
+		ok := c.lru.Add(lookupKey, lruItem{
+			size:       size,
+			sizeOnDisk: sizeOnDisk,
+			legacy:     legacy,
+			random:     random,
+		})
 		if !ok {
 			err = os.Remove(filepath.Join(c.dir, relPath))
 			if err != nil {
@@ -218,6 +492,7 @@ func (c *Cache) loadExistingFiles() error {
 	}
 
 	log.Println("Finished loading disk cache files.")
+
 	return nil
 }
 
@@ -241,9 +516,10 @@ func (c *Cache) Put(kind cache.EntryKind, hash string, size int64, r io.Reader)
 		return nil
 	}
 
-	key := cache.Key(kind, hash)
+	key := cache.LookupKey(kind, hash)
 
 	var tf *os.File // Tempfile.
+	var blobFile string
 
 	// Cleanup intermediate state if something went wrong and we
 	// did not successfully commit.
@@ -252,7 +528,10 @@ func (c *Cache) Put(kind cache.EntryKind, hash string, size int64, r io.Reader)
 	defer func() {
 		// No lock required to remove stray tempfiles.
 		if removeTempfile {
-			os.Remove(tf.Name())
+			os.Remove(blobFile)
+		} else if blobFile != "" {
+			// Mark the file as "complete".
+			os.Chmod(blobFile, tempfile.EndMode)
 		}
 
 		if unreserve {
@@ -289,38 +568,42 @@ func (c *Cache) Put(kind cache.EntryKind, hash string, size int64, r io.Reader)
 		unreserve = true
 	}
 
+	legacy := kind == cache.CAS && c.storageMode == casblob.Identity
+
 	// Final destination, if all goes well.
-	filePath := cacheFilePath(kind, c.dir, hash)
+	filePath := path.Join(c.dir, c.FileLocationBase(kind, legacy, hash, size))
 
 	// We will download to this temporary file.
-	tf, err := tfc.Create(filePath)
+	tf, random, err := tfc.Create(filePath, legacy)
+	blobFile = tf.Name()
 	if err != nil {
 		return err
 	}
 	removeTempfile = true
 
-	err = writeAndCloseFile(r, kind, hash, size, tf)
+	var sizeOnDisk int64
+	sizeOnDisk, err = c.writeAndCloseFile(r, kind, hash, size, tf)
 	if err != nil {
 		return err
 	}
 
 	if c.proxy != nil {
-		rc, err := os.Open(tf.Name())
+		rc, err := os.Open(blobFile)
 		if err != nil {
 			log.Println("Failed to proxy Put:", err)
 		} else {
 			// Doesn't block, should be fast.
-			c.proxy.Put(kind, hash, size, rc)
+			c.proxy.Put(kind, hash, sizeOnDisk, rc)
 		}
 	}
 
-	unreserve, removeTempfile, err = c.commit(key, tf.Name(), filePath, size, size)
+	unreserve, removeTempfile, err = c.commit(key, legacy, blobFile, size, size, sizeOnDisk, random)
 
 	// Might be nil.
 	return err
 }
 
-func writeAndCloseFile(r io.Reader, kind cache.EntryKind, hash string, size int64, f *os.File) error {
+func (c *Cache) writeAndCloseFile(r io.Reader, kind cache.EntryKind, hash string, size int64, f *os.File) (int64, error) {
 	closeFile := true
 	defer func() {
 		if closeFile {
@@ -329,44 +612,40 @@ func writeAndCloseFile(r io.Reader, kind cache.EntryKind, hash string, size int6
 	}()
 
 	var err error
+	var sizeOnDisk int64
 
-	var bytesCopied int64
-	if kind == cache.CAS {
-		hasher := sha256.New()
-		bytesCopied, err = io.Copy(io.MultiWriter(f, hasher), r)
+	if kind == cache.CAS && c.storageMode != casblob.Identity {
+		sizeOnDisk, err = casblob.WriteAndClose(r, f, c.storageMode, hash, size)
 		if err != nil {
-			return err
-		}
-		actualHash := hex.EncodeToString(hasher.Sum(nil))
-		if actualHash != hash {
-			return fmt.Errorf(
-				"checksums don't match. Expected %s, found %s", hash, actualHash)
-		}
-	} else {
-		if bytesCopied, err = io.Copy(f, r); err != nil {
-			return err
+			return -1, err
 		}
+		closeFile = false
+		return sizeOnDisk, nil
 	}
 
-	if isSizeMismatch(bytesCopied, size) {
-		return fmt.Errorf(
-			"sizes don't match. Expected %d, found %d", size, bytesCopied)
+	if sizeOnDisk, err = io.Copy(f, r); err != nil {
+		return -1, err
+	}
+
+	if isSizeMismatch(sizeOnDisk, size) {
+		return -1, fmt.Errorf(
+			"sizes don't match. Expected %d, found %d", size, sizeOnDisk)
 	}
 
 	if err = f.Sync(); err != nil {
-		return err
+		return -1, err
 	}
 
 	if err = f.Close(); err != nil {
-		return err
+		return -1, err
 	}
 	closeFile = false
 
-	return nil
+	return sizeOnDisk, nil
 }
 
 // This must be called when the lock is not held.
-func (c *Cache) commit(key string, tempfile string, finalPath string, reservedSize int64, foundSize int64) (unreserve bool, removeTempfile bool, err error) {
+func (c *Cache) commit(key string, legacy bool, tempfile string, reservedSize int64, logicalSize int64, sizeOnDisk int64, random string) (unreserve bool, removeTempfile bool, err error) {
 	unreserve = reservedSize > 0
 	removeTempfile = true
 
@@ -382,20 +661,20 @@ func (c *Cache) commit(key string, tempfile string, finalPath string, reservedSi
 	}
 	unreserve = false
 
-	if !c.lru.Add(key, lruItem{size: foundSize}) {
-		err = fmt.Errorf("INTERNAL ERROR: failed to add: %s, size %d", key, foundSize)
-		log.Println(err.Error())
-		return unreserve, removeTempfile, err
+	newItem := lruItem{
+		size:       logicalSize,
+		sizeOnDisk: sizeOnDisk,
+		legacy:     legacy,
+		random:     random,
 	}
 
-	err = syscall.Rename(tempfile, finalPath)
-	if err != nil {
-		log.Printf("INTERNAL ERROR: failed to rename \"%s\" to \"%s\": %v",
-			tempfile, finalPath, err)
-		log.Println("Removing", key)
-		c.lru.Remove(key)
+	if !c.lru.Add(key, newItem) {
+		err = fmt.Errorf("INTERNAL ERROR: failed to add: %s, size %d (on disk: %d): %w",
+			key, logicalSize, sizeOnDisk, err)
+		log.Println(err.Error())
 		return unreserve, removeTempfile, err
 	}
+
 	removeTempfile = false
 
 	// Commit successful if we made it this far! \o/
@@ -405,21 +684,64 @@ func (c *Cache) commit(key string, tempfile string, finalPath string, reservedSi
 // Return a non-nil io.ReadCloser and non-negative size if the item is available
 // locally, and a boolean that indicates if the item is not available locally
 // but that we can try the proxy backend.
-func (c *Cache) availableOrTryProxy(key string, size int64, blobPath string) (rc io.ReadCloser, foundSize int64, tryProxy bool, err error) {
+//
+// This function assumes that only CAS blobs are requested in zstd form.
+func (c *Cache) availableOrTryProxy(kind cache.EntryKind, hash string, size int64, offset int64, zstd bool) (rc io.ReadCloser, foundSize int64, tryProxy bool, err error) {
 	locked := true
 	c.mu.Lock()
 
+	key := cache.LookupKey(kind, hash)
 	item, available := c.lru.Get(key)
 	if available {
 		c.mu.Unlock() // We expect a cache hit below.
 		locked = false
 
+		blobPath := path.Join(c.dir, c.FileLocation(kind, item.legacy, hash, item.size, item.random))
+
 		if !isSizeMismatch(size, item.size) {
 			var f *os.File
 			f, err = os.Open(blobPath)
+			if err != nil && os.IsNotExist(err) {
+				// Another request replaced the file before we could open it?
+				// Enter slow path.
+
+				c.mu.Lock()
+				item, available = c.lru.Get(key)
+				if available {
+					blobPath = path.Join(c.dir, c.FileLocation(kind, item.legacy, hash, item.size, item.random))
+					f, err = os.Open(blobPath)
+				}
+				c.mu.Unlock()
+			}
+
 			if err != nil {
 				// Race condition, was the item purged after we released the lock?
-				log.Printf("Warning: expected %s to exist on disk, undersized cache?", blobPath)
+				log.Printf("Warning: expected %q to exist on disk, undersized cache?", blobPath)
+			} else if kind == cache.CAS {
+				if item.legacy {
+					// The file is uncompressed, without a casblob header.
+					_, err = f.Seek(offset, io.SeekStart)
+					if zstd && err == nil {
+						rc, err = casblob.GetLegacyZstdReadCloser(f)
+					} else if err == nil {
+						rc = f
+					}
+				} else {
+					// The file is compressed.
+					if zstd {
+						rc, err = casblob.GetZstdReadCloser(f, size, offset)
+					} else {
+						rc, err = casblob.GetUncompressedReadCloser(f, size, offset)
+					}
+				}
+
+				if err != nil {
+					log.Printf("Warning: expected item to be on disk, but something happened: %v", err)
+					f.Close()
+					rc = nil
+				} else {
+					return rc, item.size, false, nil
+				}
 			} else {
 				var fileInfo os.FileInfo
 				fileInfo, err = f.Stat()
@@ -429,6 +751,7 @@ func (c *Cache) availableOrTryProxy(key string, size int64, blobPath string) (rc
 					log.Printf("Warning: expected %s to on disk to have size %d, found %d",
 						blobPath, size, foundSize)
 				} else {
+					f.Seek(offset, io.SeekStart)
 					return f, foundSize, false, nil
 				}
 			}
@@ -459,13 +782,25 @@ func (c *Cache) availableOrTryProxy(key string, size int64, blobPath string) (rc
 	return nil, -1, tryProxy, err
 }
 
+var errOnlyCompressedCAS = errors.New("Only CAS blobs are available in compressed form")
+
 // Get returns an io.ReadCloser with the content of the cache item stored
 // under `hash` and the number of bytes that can be read from it. If the
 // item is not found, the io.ReadCloser will be nil. If some error occurred
 // when processing the request, then it is returned. Callers should provide
 // the `size` of the item to be retrieved, or -1 if unknown.
-func (c *Cache) Get(kind cache.EntryKind, hash string, size int64) (rc io.ReadCloser, s int64, rErr error) {
+func (c *Cache) Get(kind cache.EntryKind, hash string, size int64, offset int64) (rc io.ReadCloser, s int64, rErr error) {
+	return c.get(kind, hash, size, offset, false)
+}
 
+// GetZstd is just like Get, except the data available from rc is zstandard
+// compressed. Note that the returned `s` value still refers to the amount
+// of data once it has been decompressed.
+func (c *Cache) GetZstd(hash string, size int64, offset int64) (rc io.ReadCloser, s int64, rErr error) {
+	return c.get(cache.CAS, hash, size, offset, true)
+}
+
+func (c *Cache) get(kind cache.EntryKind, hash string, size int64, offset int64, zstd bool) (rc io.ReadCloser, s int64, rErr error) {
 	// The hash format is checked properly in the http/grpc code.
 	// Just perform a simple/fast check here, to catch bad tests.
 	if len(hash) != sha256HashStrSize {
@@ -475,13 +810,30 @@ func (c *Cache) Get(kind cache.EntryKind, hash string, size int64) (rc io.ReadCl
 
 	if kind == cache.CAS && size <= 0 && hash == emptySha256 {
 		cacheHits.Inc()
+
+		if zstd {
+			return ioutil.NopCloser(bytes.NewReader(emptyZstdBlob)), 0, nil
+		}
+
 		return ioutil.NopCloser(bytes.NewReader([]byte{})), 0, nil
 	}
 
+	if kind != cache.CAS && zstd {
+		return nil, -1, errOnlyCompressedCAS
+	}
+
+	if offset < 0 {
+		return nil, -1, fmt.Errorf("Invalid offset: %d", offset)
+	}
+	if size > 0 && offset >= size {
+		return nil, -1, fmt.Errorf("Invalid offset: %d for size %d", offset, size)
+	}
+
 	var err error
-	key := cache.Key(kind, hash)
+	key := cache.LookupKey(kind, hash)
 
 	var tf *os.File // Tempfile we will write to.
+	var blobFile string
 
 	// Cleanup intermediate state if something went wrong and we
 	// did not successfully commit.
@@ -490,7 +842,10 @@ func (c *Cache) Get(kind cache.EntryKind, hash string, size int64) (rc io.ReadCl
 	defer func() {
 		// No lock required to remove stray tempfiles.
 		if removeTempfile {
-			os.Remove(tf.Name())
+			os.Remove(blobFile)
+		} else if blobFile != "" {
+			// Mark the file as "complete".
+			os.Chmod(blobFile, tempfile.EndMode)
 		}
 
 		if unreserve {
@@ -505,8 +860,7 @@ func (c *Cache) Get(kind cache.EntryKind, hash string, size int64) (rc io.ReadCl
 		}
 	}()
 
-	blobPath := cacheFilePath(kind, c.dir, hash)
-	f, foundSize, tryProxy, err := c.availableOrTryProxy(key, size, blobPath)
+	f, foundSize, tryProxy, err := c.availableOrTryProxy(kind, hash, size, offset, zstd)
 	if err != nil {
 		return nil, -1, err
 	}
@@ -524,41 +878,71 @@ func (c *Cache) Get(kind cache.EntryKind, hash string, size int64) (rc io.ReadCl
 		return nil, -1, nil
 	}
 
-	r, foundSize, err := c.proxy.Get(kind, hash)
+	r, foundLogicalSize, err := c.proxy.Get(kind, hash)
 	if r != nil {
 		defer r.Close()
 	}
 	if err != nil || r == nil {
 		return nil, -1, err
 	}
-	if isSizeMismatch(size, foundSize) {
+
+	if isSizeMismatch(size, foundLogicalSize) {
 		return nil, -1, nil
 	}
 
-	tf, err = tfc.Create(blobPath)
+	legacy := kind == cache.CAS && c.storageMode == casblob.Identity
+
+	blobPathBase := path.Join(c.dir, c.FileLocationBase(kind, legacy, hash, foundSize))
+	tf, random, err := tfc.Create(blobPathBase, legacy)
 	if err != nil {
 		return nil, -1, err
 	}
 	removeTempfile = true
 
-	err = writeAndCloseFile(r, kind, hash, foundSize, tf)
+	blobFile = tf.Name()
+
+	var sizeOnDisk int64
+	sizeOnDisk, err = io.Copy(tf, r)
+	tf.Close()
 	if err != nil {
 		return nil, -1, err
 	}
 
-	rc, err = os.Open(tf.Name())
+	rcf, err := os.Open(blobFile)
 	if err != nil {
 		return nil, -1, err
 	}
 
-	unreserve, removeTempfile, err = c.commit(key, tf.Name(), blobPath, size, foundSize)
+	uncompressedOnDisk := (kind != cache.CAS) || (c.storageMode == casblob.Identity)
+	if uncompressedOnDisk {
+		if offset > 0 {
+			rcf.Seek(offset, io.SeekStart)
+		}
+
+		if zstd {
+			rc, err = casblob.GetLegacyZstdReadCloser(rcf)
+		} else {
+			rc = rcf
+		}
+	} else { // Compressed CAS blob.
+		if zstd {
+			rc, err = casblob.GetZstdReadCloser(rcf, foundLogicalSize, offset)
+		} else {
+			rc, err = casblob.GetUncompressedReadCloser(rcf, foundLogicalSize, offset)
+		}
+	}
+	if err != nil {
+		return nil, -1, err
+	}
+
+	unreserve, removeTempfile, err = c.commit(key, legacy, blobFile, size, foundLogicalSize, sizeOnDisk, random)
 	if err != nil {
 		rc.Close()
 		rc = nil
 		foundSize = -1
 	}
 
-	return rc, foundSize, err
+	return rc, foundLogicalSize, err
 }
 
 // Contains returns true if the `hash` key exists in the cache, and
@@ -581,7 +965,7 @@ func (c *Cache) Contains(kind cache.EntryKind, hash string, size int64) (bool, i
 	}
 
 	foundSize := int64(-1)
-	key := cache.Key(kind, hash)
+	key := cache.LookupKey(kind, hash)
 
 	c.mu.Lock()
 	item, exists := c.lru.Get(key)
@@ -612,11 +996,11 @@ func (c *Cache) MaxSize() int64 {
 
 // Stats returns the current size of the cache in bytes, and the number of
 // items stored in the cache.
-func (c *Cache) Stats() (totalSize int64, reservedSize int64, numItems int) {
+func (c *Cache) Stats() (totalSize int64, reservedSize int64, numItems int, uncompressedSize int64) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
-	return c.lru.TotalSize(), c.lru.ReservedSize(), c.lru.Len()
+	return c.lru.TotalSize(), c.lru.ReservedSize(), c.lru.Len(), c.lru.UncompressedSize()
 }
 
 func isSizeMismatch(requestedSize int64, foundSize int64) bool {
@@ -632,17 +1016,13 @@ func ensureDirExists(path string) {
 	}
 }
 
-func cacheFilePath(kind cache.EntryKind, cacheDir string, hash string) string {
-	return filepath.Join(cacheDir, cache.Key(kind, hash))
-}
-
 // GetValidatedActionResult returns a valid ActionResult and its serialized
 // value from the CAS if it and all its dependencies are also available. If
 // not, nil values are returned. If something unexpected went wrong, return
 // an error.
 func (c *Cache) GetValidatedActionResult(hash string) (*pb.ActionResult, []byte, error) {
 
-	rc, sizeBytes, err := c.Get(cache.AC, hash, -1)
+	rc, sizeBytes, err := c.Get(cache.AC, hash, -1, 0)
 	if rc != nil {
 		defer rc.Close()
 	}
@@ -675,7 +1055,7 @@ func (c *Cache) GetValidatedActionResult(hash string) (*pb.ActionResult, []byte,
 	}
 
 	for _, d := range result.OutputDirectories {
-		r, size, err := c.Get(cache.CAS, d.TreeDigest.Hash, d.TreeDigest.SizeBytes)
+		r, size, err := c.Get(cache.CAS, d.TreeDigest.Hash, d.TreeDigest.SizeBytes, 0)
 		if r == nil {
 			return nil, nil, err // aka "not found", or an err if non-nil
 		}
diff --git a/cache/disk/lru.go b/cache/disk/lru.go
index 67165b5..f8984be 100644
--- a/cache/disk/lru.go
+++ b/cache/disk/lru.go
@@ -15,6 +15,11 @@ var (
 		Help: "The current number of bytes in the disk backend",
 	})
 
+	gaugeCacheLogicalBytes = promauto.NewGauge(prometheus.GaugeOpts{
+		Name: "bazel_remote_disk_cache_logical_bytes",
+		Help: "The current number of bytes in the disk backend if they were uncompressed",
+	})
+
 	counterEvictedBytes = promauto.NewCounter(prometheus.CounterOpts{
 		Name: "bazel_remote_disk_cache_evicted_bytes_total",
 		Help: "The total number of bytes evicted from disk backend, due to full cache",
@@ -50,13 +55,18 @@ type SizedLRU struct {
 	// Map to access the items in O(1) time
 	cache map[interface{}]*list.Element
 
-	// Includes reserved size.
+	// Total cache size including reserved bytes.
 	currentSize int64
 
+	// Total size of all blobs in uncompressed form.
+	// This does not include reserved space.
+	uncompressedSize int64
+
+	// Number of bytes reserved for incoming blobs.
 	reservedSize int64
 
-	// SizedLRU will evict items as needed to maintain the total size of the cache
-	// below maxSize.
+	// SizedLRU will evict items as needed to maintain the total size of the
+	// cache below maxSize.
 	maxSize int64
 
 	onEvict EvictCallback
@@ -81,24 +91,32 @@ func NewSizedLRU(maxSize int64, onEvict EvictCallback) SizedLRU {
 // and does not add the item) if the item size is larger than the maximum size of the cache,
 // or it cannot be added to the cache because too much space is reserved.
 func (c *SizedLRU) Add(key Key, value lruItem) (ok bool) {
-	if value.size > c.maxSize {
+	if value.sizeOnDisk > c.maxSize {
 		return false
 	}
 
-	var sizeDelta int64
+	var sizeDelta, uncompressedSizeDelta int64
 	if ee, ok := c.cache[key]; ok {
-		sizeDelta = value.size - ee.Value.(*entry).value.size
+		sizeDelta = value.sizeOnDisk - ee.Value.(*entry).value.sizeOnDisk
 		if c.reservedSize+sizeDelta > c.maxSize {
 			return false
 		}
+		uncompressedSizeDelta = value.size - ee.Value.(*entry).value.size
 		c.ll.MoveToFront(ee)
-		counterOverwrittenBytes.Add(float64(ee.Value.(*entry).value.size))
+		counterOverwrittenBytes.Add(float64(ee.Value.(*entry).value.sizeOnDisk))
+
+		prevValue := ee.Value.(*entry).value
+		if c.onEvict != nil {
+			c.onEvict(key, prevValue)
+		}
+
 		ee.Value.(*entry).value = value
 	} else {
-		sizeDelta = value.size
+		sizeDelta = value.sizeOnDisk
 		if c.reservedSize+sizeDelta > c.maxSize {
 			return false
 		}
+		uncompressedSizeDelta = value.size
 		ele := c.ll.PushFront(&entry{key, value})
 		c.cache[key] = ele
 	}
@@ -113,8 +131,10 @@ func (c *SizedLRU) Add(key Key, value lruItem) (ok bool) {
 	}
 
 	c.currentSize += sizeDelta
+	c.uncompressedSize += uncompressedSizeDelta
 
 	gaugeCacheSizeBytes.Set(float64(c.currentSize))
+	gaugeCacheLogicalBytes.Set(float64(c.uncompressedSize))
 
 	return true
 }
@@ -134,6 +154,7 @@ func (c *SizedLRU) Remove(key Key) {
 	if ele, hit := c.cache[key]; hit {
 		c.removeElement(ele)
 		gaugeCacheSizeBytes.Set(float64(c.currentSize))
+		gaugeCacheLogicalBytes.Set(float64(c.uncompressedSize))
 	}
 }
 
@@ -146,6 +167,10 @@ func (c *SizedLRU) TotalSize() int64 {
 	return c.currentSize
 }
 
+func (c *SizedLRU) UncompressedSize() int64 {
+	return c.uncompressedSize
+}
+
 func (c *SizedLRU) ReservedSize() int64 {
 	return c.reservedSize
 }
@@ -228,8 +253,9 @@ func (c *SizedLRU) removeElement(e *list.Element) {
 	c.ll.Remove(e)
 	kv := e.Value.(*entry)
 	delete(c.cache, kv.key)
-	c.currentSize -= kv.value.size
-	counterEvictedBytes.Add(float64(kv.value.size))
+	c.currentSize -= kv.value.sizeOnDisk
+	c.uncompressedSize -= kv.value.size
+	counterEvictedBytes.Add(float64(kv.value.sizeOnDisk))
 
 	if c.onEvict != nil {
 		c.onEvict(kv.key, kv.value)
diff --git a/cache/gcsproxy/gcsproxy.go b/cache/gcsproxy/gcsproxy.go
index 74444e3..f5ed4ef 100644
--- a/cache/gcsproxy/gcsproxy.go
+++ b/cache/gcsproxy/gcsproxy.go
@@ -16,7 +16,7 @@ import (
 )
 
 // New creates a cache that proxies requests to Google Cloud Storage.
-func New(bucket string, useDefaultCredentials bool, jsonCredentialsFile string,
+func New(bucket string, useDefaultCredentials bool, jsonCredentialsFile string, storageMode string,
 	accessLogger cache.Logger, errorLogger cache.Logger, numUploaders, maxQueuedUploads int) (cache.Proxy, error) {
 	var remoteClient *http.Client
 	var err error
@@ -54,5 +54,5 @@ func New(bucket string, useDefaultCredentials bool, jsonCredentialsFile string,
 		Path:   bucket,
 	}
 
-	return httpproxy.New(&baseURL, remoteClient, accessLogger, errorLogger, numUploaders, maxQueuedUploads), nil
+	return httpproxy.New(&baseURL, storageMode, remoteClient, accessLogger, errorLogger, numUploaders, maxQueuedUploads)
 }
diff --git a/cache/httpproxy/BUILD.bazel b/cache/httpproxy/BUILD.bazel
index c252723..6d8c89b 100644
--- a/cache/httpproxy/BUILD.bazel
+++ b/cache/httpproxy/BUILD.bazel
@@ -7,6 +7,7 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//cache:go_default_library",
+        "//cache/disk/casblob:go_default_library",
         "@com_github_prometheus_client_golang//prometheus:go_default_library",
         "@com_github_prometheus_client_golang//prometheus/promauto:go_default_library",
     ],
@@ -19,6 +20,7 @@ go_test(
     deps = [
         "//cache:go_default_library",
         "//cache/disk:go_default_library",
+        "//cache/disk/casblob:go_default_library",
         "//utils:go_default_library",
     ],
 )
diff --git a/cache/httpproxy/httpproxy.go b/cache/httpproxy/httpproxy.go
index d640a22..baee112 100644
--- a/cache/httpproxy/httpproxy.go
+++ b/cache/httpproxy/httpproxy.go
@@ -10,8 +10,10 @@ import (
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
 
 	"github.com/buchgr/bazel-remote/cache"
+	"github.com/buchgr/bazel-remote/cache/disk/casblob"
 
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
@@ -26,10 +28,12 @@ type uploadReq struct {
 
 type remoteHTTPProxyCache struct {
 	remote       *http.Client
-	baseURL      *url.URL
+	baseURL      string
 	uploadQueue  chan<- uploadReq
 	accessLogger cache.Logger
 	errorLogger  cache.Logger
+	requestURL   func(hash string, kind cache.EntryKind) string
+	v2mode       bool
 }
 
 var (
@@ -43,8 +47,7 @@ var (
 	})
 )
 
-func uploadFile(remote *http.Client, baseURL *url.URL, accessLogger cache.Logger,
-	errorLogger cache.Logger, item uploadReq) {
+func (r *remoteHTTPProxyCache) uploadFile(item uploadReq) {
 
 	if item.size == 0 {
 		item.rc.Close()
@@ -52,11 +55,11 @@ func uploadFile(remote *http.Client, baseURL *url.URL, accessLogger cache.Logger
 		item.rc = http.NoBody
 	}
 
-	url := requestURL(baseURL, item.hash, item.kind)
+	url := r.requestURL(item.hash, item.kind)
 
-	rsp, err := remote.Head(url)
+	rsp, err := r.remote.Head(url)
 	if err == nil && rsp.StatusCode == http.StatusOK {
-		accessLogger.Printf("SKIP UPLOAD %s", item.hash)
+		r.accessLogger.Printf("SKIP UPLOAD %s", item.hash)
 		return
 	}
 
@@ -71,44 +74,64 @@ func uploadFile(remote *http.Client, baseURL *url.URL, accessLogger cache.Logger
 	req.Header.Set("Content-Type", "application/octet-stream")
 	req.ContentLength = item.size
 
-	rsp, err = remote.Do(req)
+	rsp, err = r.remote.Do(req)
 	if err != nil {
 		return
 	}
 	io.Copy(ioutil.Discard, rsp.Body)
 	rsp.Body.Close()
 
-	logResponse(accessLogger, "UPLOAD", rsp.StatusCode, url)
+	logResponse(r.accessLogger, "UPLOAD", rsp.StatusCode, url)
 	return
 }
 
 // New creates a cache that proxies requests to a HTTP remote cache.
-func New(baseURL *url.URL, remote *http.Client, accessLogger cache.Logger,
-	errorLogger cache.Logger, numUploaders, maxQueuedUploads int) cache.Proxy {
+// `storageMode` must be one of "uncompressed" (which expects legacy
+// CAS blobs) or "zstd" (which expects cas.v2 blobs).
+func New(baseURL *url.URL, storageMode string, remote *http.Client,
+	accessLogger cache.Logger, errorLogger cache.Logger,
+	numUploaders, maxQueuedUploads int) (cache.Proxy, error) {
 
 	proxy := &remoteHTTPProxyCache{
 		remote:       remote,
-		baseURL:      baseURL,
+		baseURL:      strings.TrimRight(baseURL.String(), "/"),
 		accessLogger: accessLogger,
 		errorLogger:  errorLogger,
+		v2mode:       storageMode == "zstd",
+	}
+
+	if storageMode == "zstd" {
+		proxy.requestURL = func(hash string, kind cache.EntryKind) string {
+			if kind == cache.CAS {
+				return fmt.Sprintf("%s/cas.v2/%s", proxy.baseURL, hash)
+			}
+
+			return fmt.Sprintf("%s/%s/%s", proxy.baseURL, kind, hash)
+		}
+	} else if storageMode == "uncompressed" {
+		proxy.requestURL = func(hash string, kind cache.EntryKind) string {
+			return fmt.Sprintf("%s/%s/%s", proxy.baseURL, kind, hash)
+		}
+	} else {
+		return nil, fmt.Errorf("Invalid http_proxy.mode specified: %q",
+			storageMode)
 	}
 
 	if maxQueuedUploads > 0 && numUploaders > 0 {
 		uploadQueue := make(chan uploadReq, maxQueuedUploads)
 
 		for i := 0; i < numUploaders; i++ {
-			go func(remote *http.Client, baseURL *url.URL, accessLogger cache.Logger,
-				errorLogger cache.Logger) {
+			go func() {
 				for item := range uploadQueue {
-					uploadFile(remote, baseURL, accessLogger, errorLogger, item)
+					proxy.uploadFile(item)
 				}
-			}(remote, baseURL, accessLogger, errorLogger)
+			}()
 		}
 
 		proxy.uploadQueue = uploadQueue
 	}
 
-	return proxy
+	return proxy, nil
 }
 
 // Helper function for logging responses
@@ -122,13 +145,15 @@ func (r *remoteHTTPProxyCache) Put(kind cache.EntryKind, hash string, size int64
 		return
 	}
 
-	select {
-	case r.uploadQueue <- uploadReq{
+	item := uploadReq{
 		hash: hash,
 		size: size,
 		kind: kind,
 		rc:   rc,
-	}:
+	}
+
+	select {
+	case r.uploadQueue <- item:
 	default:
 		r.errorLogger.Printf("too many uploads queued")
 		rc.Close()
@@ -136,7 +161,7 @@ func (r *remoteHTTPProxyCache) Put(kind cache.EntryKind, hash string, size int64
 }
 
 func (r *remoteHTTPProxyCache) Get(kind cache.EntryKind, hash string) (io.ReadCloser, int64, error) {
-	url := requestURL(r.baseURL, hash, kind)
+	url := r.requestURL(hash, kind)
 	rsp, err := r.remote.Get(url)
 	if err != nil {
 		cacheMisses.Inc()
@@ -167,6 +192,11 @@ func (r *remoteHTTPProxyCache) Get(kind cache.EntryKind, hash string) (io.ReadCl
 		}
 	}
 
+	if kind == cache.CAS && r.v2mode {
+		cacheHits.Inc()
+		return casblob.ExtractLogicalSize(rsp.Body)
+	}
+
 	sizeBytesStr := rsp.Header.Get("Content-Length")
 	if sizeBytesStr == "" {
 		err = errors.New("Missing Content-Length header")
@@ -183,21 +213,24 @@ func (r *remoteHTTPProxyCache) Get(kind cache.EntryKind, hash string) (io.ReadCl
 
 	cacheHits.Inc()
 
-	return rsp.Body, sizeBytes, err
+	return rsp.Body, sizeBytes, nil
 }
 
 func (r *remoteHTTPProxyCache) Contains(kind cache.EntryKind, hash string) (bool, int64) {
 
-	url := requestURL(r.baseURL, hash, kind)
+	url := r.requestURL(hash, kind)
 
 	rsp, err := r.remote.Head(url)
 	if err == nil && rsp.StatusCode == http.StatusOK {
-		return true, rsp.ContentLength
-	}
+		if kind != cache.CAS {
+			return true, rsp.ContentLength
+		}
 
-	return false, int64(-1)
-}
+		// We don't know the content size without reading the file header
+		// and that could be very costly for the backend server. So return
+		// "unknown size".
+		return true, -1
+	}
 
-func requestURL(baseURL *url.URL, hash string, kind cache.EntryKind) string {
-	return fmt.Sprintf("%s/%s/%s", baseURL, kind, hash)
+	return false, -1
 }
diff --git a/cache/s3proxy/BUILD.bazel b/cache/s3proxy/BUILD.bazel
index 14612eb..fa2f91d 100644
--- a/cache/s3proxy/BUILD.bazel
+++ b/cache/s3proxy/BUILD.bazel
@@ -7,6 +7,7 @@ go_library(
     visibility = ["//visibility:public"],
     deps = [
         "//cache:go_default_library",
+        "//cache/disk/casblob:go_default_library",
         "//config:go_default_library",
         "@com_github_minio_minio_go_v7//:go_default_library",
         "@com_github_minio_minio_go_v7//pkg/credentials:go_default_library",
diff --git a/cache/s3proxy/s3proxy.go b/cache/s3proxy/s3proxy.go
index 32377a5..dd04d20 100644
--- a/cache/s3proxy/s3proxy.go
+++ b/cache/s3proxy/s3proxy.go
@@ -6,8 +6,10 @@ import (
 	"fmt"
 	"io"
 	"log"
+	"path"
 
 	"github.com/buchgr/bazel-remote/cache"
+	"github.com/buchgr/bazel-remote/cache/disk/casblob"
 	"github.com/buchgr/bazel-remote/config"
 	"github.com/minio/minio-go/v7"
 	"github.com/minio/minio-go/v7/pkg/credentials"
@@ -30,6 +32,8 @@ type s3Cache struct {
 	uploadQueue  chan<- uploadReq
 	accessLogger cache.Logger
 	errorLogger  cache.Logger
+	v2mode       bool
+	objectKey    func(hash string, kind cache.EntryKind) string
 }
 
 var (
@@ -47,7 +51,7 @@ var (
 var errNotFound = errors.New("NOT FOUND")
 
 // New returns a new instance of the S3-API based cache
-func New(s3Config *config.S3CloudStorageConfig, accessLogger cache.Logger,
+func New(s3Config *config.S3CloudStorageConfig, storageMode string, accessLogger cache.Logger,
 	errorLogger cache.Logger, numUploaders, maxQueuedUploads int) cache.Proxy {
 
 	fmt.Println("Using S3 backend.")
@@ -89,6 +93,11 @@ func New(s3Config *config.S3CloudStorageConfig, accessLogger cache.Logger,
 		}
 	}
 
+	if storageMode != "zstd" && storageMode != "uncompressed" {
+		log.Fatalf("Unsupported storage mode for the s3proxy backend: %q, must be one of \"zstd\" or \"uncompressed\"",
+			storageMode)
+	}
+
 	c := &s3Cache{
 		mcore:        minioCore,
 		prefix:       s3Config.Prefix,
@@ -96,6 +105,17 @@ func New(s3Config *config.S3CloudStorageConfig, accessLogger cache.Logger,
 		keyVersion:   s3Config.KeyVersion,
 		accessLogger: accessLogger,
 		errorLogger:  errorLogger,
+		v2mode:       storageMode == "zstd",
+	}
+
+	if c.v2mode {
+		c.objectKey = func(hash string, kind cache.EntryKind) string {
+			return objectKeyV2(c.prefix, hash, kind)
+		}
+	} else {
+		c.objectKey = func(hash string, kind cache.EntryKind) string {
+			return objectKeyV1(c.prefix, hash, kind)
+		}
 	}
 
 	if maxQueuedUploads > 0 && numUploaders > 0 {
@@ -114,17 +134,28 @@ func New(s3Config *config.S3CloudStorageConfig, accessLogger cache.Logger,
 	return c
 }
 
-func (c *s3Cache) objectKey(hash string, kind cache.EntryKind) string {
-	baseKey := fmt.Sprintf("%s/%s", kind, hash)
-	if c.keyVersion == 2 {
-		baseKey = cache.Key(kind, hash)
+func objectKeyV2(prefix string, hash string, kind cache.EntryKind) string {
+	var baseKey string
+	if kind == cache.CAS {
+		// Use "cas.v2" to distinguish new from old format blobs.
+		baseKey = path.Join("cas.v2", hash[:2], hash)
+	} else {
+		baseKey = path.Join(kind.String(), hash[:2], hash)
 	}
 
-	if c.prefix == "" {
+	if prefix == "" {
 		return baseKey
 	}
 
-	return fmt.Sprintf("%s/%s", c.prefix, baseKey)
+	return path.Join(prefix, baseKey)
+}
+
+func objectKeyV1(prefix string, hash string, kind cache.EntryKind) string {
+	if prefix == "" {
+		return path.Join(kind.String(), hash[:2], hash)
+	}
+
+	return path.Join(prefix, kind.String(), hash[:2], hash)
 }
 
 // Helper function for logging responses
@@ -138,11 +169,6 @@ func logResponse(log cache.Logger, method, bucket, key string, err error) {
 }
 
 func (c *s3Cache) uploadFile(item uploadReq) {
-	uploadDigest := ""
-	if item.kind == cache.CAS {
-		uploadDigest = item.hash
-	}
-
 	_, err := c.mcore.PutObject(
 		context.Background(),
 		c.bucket,                          // bucketName
@@ -150,7 +176,7 @@ func (c *s3Cache) uploadFile(item uploadReq) {
 		item.rc,                           // reader
 		item.size,                         // objectSize
 		"",                                // md5base64
-		uploadDigest,                      // sha256
+		"",                                // sha256
 		minio.PutObjectOptions{
 			UserMetadata: map[string]string{
 				"Content-Type": "application/octet-stream",
@@ -184,7 +210,7 @@ func (c *s3Cache) Put(kind cache.EntryKind, hash string, size int64, rc io.ReadC
 
 func (c *s3Cache) Get(kind cache.EntryKind, hash string) (io.ReadCloser, int64, error) {
 
-	object, info, _, err := c.mcore.GetObject(
+	rc, info, _, err := c.mcore.GetObject(
 		context.Background(),
 		c.bucket,                 // bucketName
 		c.objectKey(hash, kind),  // objectName
@@ -204,11 +230,16 @@ func (c *s3Cache) Get(kind cache.EntryKind, hash string) (io.ReadCloser, int64,
 
 	logResponse(c.accessLogger, "DOWNLOAD", c.bucket, c.objectKey(hash, kind), nil)
 
-	return object, info.Size, nil
+	if kind == cache.CAS && c.v2mode {
+		return casblob.ExtractLogicalSize(rc)
+	}
+
+	return rc, info.Size, nil
 }
 
 func (c *s3Cache) Contains(kind cache.EntryKind, hash string) (bool, int64) {
 	size := int64(-1)
+	exists := false
 
 	s, err := c.mcore.StatObject(
 		context.Background(),
@@ -217,10 +248,10 @@ func (c *s3Cache) Contains(kind cache.EntryKind, hash string) (bool, int64) {
 		minio.StatObjectOptions{}, // opts
 	)
 
-	exists := (err == nil)
+	exists = (err == nil)
 	if err != nil {
 		err = errNotFound
-	} else {
+	} else if kind != cache.CAS || !c.v2mode {
 		size = s.Size
 	}
 
diff --git a/config/config.go b/config/config.go
index 2abd9b6..66051fc 100644
--- a/config/config.go
+++ b/config/config.go
@@ -45,6 +45,7 @@ type Config struct {
 	ProfilePort                 int                       `yaml:"profile_port"`
 	Dir                         string                    `yaml:"dir"`
 	MaxSize                     int                       `yaml:"max_size"`
+	StorageMode                 string                    `yaml:"storage_mode"`
 	HtpasswdFile                string                    `yaml:"htpasswd_file"`
 	TLSCaFile                   string                    `yaml:"tls_ca_file"`
 	TLSCertFile                 string                    `yaml:"tls_cert_file"`
@@ -69,7 +70,8 @@ var defaultDurationBuckets = []float64{.5, 1, 2.5, 5, 10, 20, 40, 80, 160, 320}
 
 // New returns a validated Config with the specified values, and an error
 // if there were any problems with the validation.
-func New(dir string, maxSize int, host string, port int, grpcPort int,
+func New(dir string, maxSize int, storageMode string,
+	host string, port int, grpcPort int,
 	profileHost string, profilePort int,
 	htpasswdFile string,
 	maxQueuedUploads int,
@@ -78,6 +80,8 @@ func New(dir string, maxSize int, host string, port int, grpcPort int,
 	tlsCertFile string,
 	tlsKeyFile string,
 	idleTimeout time.Duration,
+	hc *HTTPBackendConfig,
+	gcs *GoogleCloudStorageConfig,
 	s3 *S3CloudStorageConfig,
 	disableHTTPACValidation bool,
 	disableGRPCACDepsCheck bool,
@@ -86,6 +90,7 @@ func New(dir string, maxSize int, host string, port int, grpcPort int,
 	experimentalRemoteAssetAPI bool,
 	httpReadTimeout time.Duration,
 	httpWriteTimeout time.Duration) (*Config, error) {
+
 	c := Config{
 		Host:                        host,
 		Port:                        port,
@@ -94,6 +99,7 @@ func New(dir string, maxSize int, host string, port int, grpcPort int,
 		ProfilePort:                 profilePort,
 		Dir:                         dir,
 		MaxSize:                     maxSize,
+		StorageMode:                 storageMode,
 		HtpasswdFile:                htpasswdFile,
 		MaxQueuedUploads:            maxQueuedUploads,
 		NumUploaders:                numUploaders,
@@ -101,8 +107,8 @@ func New(dir string, maxSize int, host string, port int, grpcPort int,
 		TLSCertFile:                 tlsCertFile,
 		TLSKeyFile:                  tlsKeyFile,
 		S3CloudStorage:              s3,
-		GoogleCloudStorage:          nil,
-		HTTPBackend:                 nil,
+		GoogleCloudStorage:          gcs,
+		HTTPBackend:                 hc,
 		IdleTimeout:                 idleTimeout,
 		DisableHTTPACValidation:     disableHTTPACValidation,
 		DisableGRPCACDepsCheck:      disableGRPCACDepsCheck,
@@ -141,6 +147,7 @@ func NewFromYamlFile(path string) (*Config, error) {
 
 func newFromYaml(data []byte) (*Config, error) {
 	c := Config{
+		StorageMode:            "zstd",
 		NumUploaders:           100,
 		MaxQueuedUploads:       1000000,
 		MetricsDurationBuckets: defaultDurationBuckets,
@@ -172,6 +179,25 @@ func validateConfig(c *Config) error {
 		return errors.New("The 'max_size' flag/key must be set to a value > 0")
 	}
 
+	if c.StorageMode != "zstd" && c.StorageMode != "uncompressed" {
+		return errors.New("storage_mode must be set to either \"zstd\" or \"uncompressed\"")
+	}
+
+	proxyCount := 0
+	if c.S3CloudStorage != nil {
+		proxyCount++
+	}
+	if c.HTTPBackend != nil {
+		proxyCount++
+	}
+	if c.GoogleCloudStorage != nil {
+		proxyCount++
+	}
+
+	if proxyCount > 1 {
+		return errors.New("At most one of the S3/GCS/HTTP proxy backends is allowed")
+	}
+
 	if c.Port == 0 {
 		return errors.New("A valid 'port' flag/key must be specified")
 	}
diff --git a/genproto/build/bazel/remote/execution/v2/BUILD.bazel b/genproto/build/bazel/remote/execution/v2/BUILD.bazel
index fd153ac..5ef2a7a 100644
--- a/genproto/build/bazel/remote/execution/v2/BUILD.bazel
+++ b/genproto/build/bazel/remote/execution/v2/BUILD.bazel
@@ -11,6 +11,7 @@ go_library(
         "@go_googleapis//google/api:annotations_go_proto",
         "@go_googleapis//google/longrunning:longrunning_go_proto",
         "@go_googleapis//google/rpc:status_go_proto",
+        "@io_bazel_rules_go//proto/wkt:any_go_proto",
         "@io_bazel_rules_go//proto/wkt:duration_go_proto",
         "@io_bazel_rules_go//proto/wkt:timestamp_go_proto",
         "@io_bazel_rules_go//proto/wkt:wrappers_go_proto",
diff --git a/genproto/build/bazel/remote/execution/v2/remote_execution.pb.go b/genproto/build/bazel/remote/execution/v2/remote_execution.pb.go
index 70c5ad2..8cec956 100755
--- a/genproto/build/bazel/remote/execution/v2/remote_execution.pb.go
+++ b/genproto/build/bazel/remote/execution/v2/remote_execution.pb.go
@@ -8,6 +8,7 @@ import (
 	fmt "fmt"
 	semver "github.com/buchgr/bazel-remote/genproto/build/bazel/semver"
 	proto "github.com/golang/protobuf/proto"
+	any "github.com/golang/protobuf/ptypes/any"
 	duration "github.com/golang/protobuf/ptypes/duration"
 	timestamp "github.com/golang/protobuf/ptypes/timestamp"
 	wrappers "github.com/golang/protobuf/ptypes/wrappers"
@@ -159,6 +160,34 @@ func (SymlinkAbsolutePathStrategy_Value) EnumDescriptor() ([]byte, []int) {
 	return fileDescriptor_c43847ba40caac95, []int{39, 0}
 }
 
+type Compressor_Value int32
+
+const (
+	// No compression. Servers and clients MUST always support this, and do
+	// not need to advertise it.
+	Compressor_IDENTITY Compressor_Value = 0
+	// Zstandard compression.
+	Compressor_ZSTD Compressor_Value = 1
+)
+
+var Compressor_Value_name = map[int32]string{
+	0: "IDENTITY",
+	1: "ZSTD",
+}
+
+var Compressor_Value_value = map[string]int32{
+	"IDENTITY": 0,
+	"ZSTD":     1,
+}
+
+func (x Compressor_Value) String() string {
+	return proto.EnumName(Compressor_Value_name, int32(x))
+}
+
+func (Compressor_Value) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_c43847ba40caac95, []int{40, 0}
+}
+
 // An `Action` captures all the information about an execution which is required
 // to reproduce it.
 //
@@ -1237,9 +1266,13 @@ type ExecutedActionMetadata struct {
 	OutputUploadStartTimestamp *timestamp.Timestamp `protobuf:"bytes,9,opt,name=output_upload_start_timestamp,json=outputUploadStartTimestamp,proto3" json:"output_upload_start_timestamp,omitempty"`
 	// When the worker finished uploading action outputs.
 	OutputUploadCompletedTimestamp *timestamp.Timestamp `protobuf:"bytes,10,opt,name=output_upload_completed_timestamp,json=outputUploadCompletedTimestamp,proto3" json:"output_upload_completed_timestamp,omitempty"`
-	XXX_NoUnkeyedLiteral           struct{}             `json:"-"`
-	XXX_unrecognized               []byte               `json:"-"`
-	XXX_sizecache                  int32                `json:"-"`
+	// Details that are specific to the kind of worker used. For example,
+	// on POSIX-like systems this could contain a message with
+	// getrusage(2) statistics.
+	AuxiliaryMetadata    []*any.Any `protobuf:"bytes,11,rep,name=auxiliary_metadata,json=auxiliaryMetadata,proto3" json:"auxiliary_metadata,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
+	XXX_unrecognized     []byte     `json:"-"`
+	XXX_sizecache        int32      `json:"-"`
 }
 
 func (m *ExecutedActionMetadata) Reset()         { *m = ExecutedActionMetadata{} }
@@ -1337,6 +1370,13 @@ func (m *ExecutedActionMetadata) GetOutputUploadCompletedTimestamp() *timestamp.
 	return nil
 }
 
+func (m *ExecutedActionMetadata) GetAuxiliaryMetadata() []*any.Any {
+	if m != nil {
+		return m.AuxiliaryMetadata
+	}
+	return nil
+}
+
 // An ActionResult represents the result of an
 // [Action][build.bazel.remote.execution.v2.Action] being run.
 //
@@ -3442,6 +3482,38 @@ func (m *SymlinkAbsolutePathStrategy) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_SymlinkAbsolutePathStrategy proto.InternalMessageInfo
 
+// Compression formats which may be supported.
+type Compressor struct {
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *Compressor) Reset()         { *m = Compressor{} }
+func (m *Compressor) String() string { return proto.CompactTextString(m) }
+func (*Compressor) ProtoMessage()    {}
+func (*Compressor) Descriptor() ([]byte, []int) {
+	return fileDescriptor_c43847ba40caac95, []int{40}
+}
+
+func (m *Compressor) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Compressor.Unmarshal(m, b)
+}
+func (m *Compressor) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Compressor.Marshal(b, m, deterministic)
+}
+func (m *Compressor) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Compressor.Merge(m, src)
+}
+func (m *Compressor) XXX_Size() int {
+	return xxx_messageInfo_Compressor.Size(m)
+}
+func (m *Compressor) XXX_DiscardUnknown() {
+	xxx_messageInfo_Compressor.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Compressor proto.InternalMessageInfo
+
 // Capabilities of the remote cache system.
 type CacheCapabilities struct {
 	// All the digest functions supported by the remote cache.
@@ -3458,16 +3530,23 @@ type CacheCapabilities struct {
 	MaxBatchTotalSizeBytes int64 `protobuf:"varint,4,opt,name=max_batch_total_size_bytes,json=maxBatchTotalSizeBytes,proto3" json:"max_batch_total_size_bytes,omitempty"`
 	// Whether absolute symlink targets are supported.
 	SymlinkAbsolutePathStrategy SymlinkAbsolutePathStrategy_Value `protobuf:"varint,5,opt,name=symlink_absolute_path_strategy,json=symlinkAbsolutePathStrategy,proto3,enum=build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy_Value" json:"symlink_absolute_path_strategy,omitempty"`
-	XXX_NoUnkeyedLiteral        struct{}                          `json:"-"`
-	XXX_unrecognized            []byte                            `json:"-"`
-	XXX_sizecache               int32                             `json:"-"`
+	// Compressors supported by the "compressed-blobs" bytestream resources.
+	// Servers MUST support identity/no-compression, even if it is not listed
+	// here.
+	//
+	// Note that this does not imply which if any compressors are supported by
+	// the server at the gRPC level.
+	SupportedCompressor  []Compressor_Value `protobuf:"varint,6,rep,packed,name=supported_compressor,json=supportedCompressor,proto3,enum=build.bazel.remote.execution.v2.Compressor_Value" json:"supported_compressor,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}           `json:"-"`
+	XXX_unrecognized     []byte             `json:"-"`
+	XXX_sizecache        int32              `json:"-"`
 }
 
 func (m *CacheCapabilities) Reset()         { *m = CacheCapabilities{} }
 func (m *CacheCapabilities) String() string { return proto.CompactTextString(m) }
 func (*CacheCapabilities) ProtoMessage()    {}
 func (*CacheCapabilities) Descriptor() ([]byte, []int) {
-	return fileDescriptor_c43847ba40caac95, []int{40}
+	return fileDescriptor_c43847ba40caac95, []int{41}
 }
 
 func (m *CacheCapabilities) XXX_Unmarshal(b []byte) error {
@@ -3523,6 +3602,13 @@ func (m *CacheCapabilities) GetSymlinkAbsolutePathStrategy() SymlinkAbsolutePath
 	return SymlinkAbsolutePathStrategy_UNKNOWN
 }
 
+func (m *CacheCapabilities) GetSupportedCompressor() []Compressor_Value {
+	if m != nil {
+		return m.SupportedCompressor
+	}
+	return nil
+}
+
 // Capabilities of the remote execution system.
 type ExecutionCapabilities struct {
 	// Remote execution may only support a single digest function.
@@ -3542,7 +3628,7 @@ func (m *ExecutionCapabilities) Reset()         { *m = ExecutionCapabilities{} }
 func (m *ExecutionCapabilities) String() string { return proto.CompactTextString(m) }
 func (*ExecutionCapabilities) ProtoMessage()    {}
 func (*ExecutionCapabilities) Descriptor() ([]byte, []int) {
-	return fileDescriptor_c43847ba40caac95, []int{41}
+	return fileDescriptor_c43847ba40caac95, []int{42}
 }
 
 func (m *ExecutionCapabilities) XXX_Unmarshal(b []byte) error {
@@ -3606,7 +3692,7 @@ func (m *ToolDetails) Reset()         { *m = ToolDetails{} }
 func (m *ToolDetails) String() string { return proto.CompactTextString(m) }
 func (*ToolDetails) ProtoMessage()    {}
 func (*ToolDetails) Descriptor() ([]byte, []int) {
-	return fileDescriptor_c43847ba40caac95, []int{42}
+	return fileDescriptor_c43847ba40caac95, []int{43}
 }
 
 func (m *ToolDetails) XXX_Unmarshal(b []byte) error {
@@ -3675,7 +3761,7 @@ func (m *RequestMetadata) Reset()         { *m = RequestMetadata{} }
 func (m *RequestMetadata) String() string { return proto.CompactTextString(m) }
 func (*RequestMetadata) ProtoMessage()    {}
 func (*RequestMetadata) Descriptor() ([]byte, []int) {
-	return fileDescriptor_c43847ba40caac95, []int{43}
+	return fileDescriptor_c43847ba40caac95, []int{44}
 }
 
 func (m *RequestMetadata) XXX_Unmarshal(b []byte) error {
@@ -3728,6 +3814,7 @@ func init() {
 	proto.RegisterEnum("build.bazel.remote.execution.v2.ExecutionStage_Value", ExecutionStage_Value_name, ExecutionStage_Value_value)
 	proto.RegisterEnum("build.bazel.remote.execution.v2.DigestFunction_Value", DigestFunction_Value_name, DigestFunction_Value_value)
 	proto.RegisterEnum("build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy_Value", SymlinkAbsolutePathStrategy_Value_name, SymlinkAbsolutePathStrategy_Value_value)
+	proto.RegisterEnum("build.bazel.remote.execution.v2.Compressor_Value", Compressor_Value_name, Compressor_Value_value)
 	proto.RegisterType((*Action)(nil), "build.bazel.remote.execution.v2.Action")
 	proto.RegisterType((*Command)(nil), "build.bazel.remote.execution.v2.Command")
 	proto.RegisterType((*Command_EnvironmentVariable)(nil), "build.bazel.remote.execution.v2.Command.EnvironmentVariable")
@@ -3775,6 +3862,7 @@ func init() {
 	proto.RegisterType((*PriorityCapabilities)(nil), "build.bazel.remote.execution.v2.PriorityCapabilities")
 	proto.RegisterType((*PriorityCapabilities_PriorityRange)(nil), "build.bazel.remote.execution.v2.PriorityCapabilities.PriorityRange")
 	proto.RegisterType((*SymlinkAbsolutePathStrategy)(nil), "build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy")
+	proto.RegisterType((*Compressor)(nil), "build.bazel.remote.execution.v2.Compressor")
 	proto.RegisterType((*CacheCapabilities)(nil), "build.bazel.remote.execution.v2.CacheCapabilities")
 	proto.RegisterType((*ExecutionCapabilities)(nil), "build.bazel.remote.execution.v2.ExecutionCapabilities")
 	proto.RegisterType((*ToolDetails)(nil), "build.bazel.remote.execution.v2.ToolDetails")
@@ -3786,220 +3874,226 @@ func init() {
 }
 
 var fileDescriptor_c43847ba40caac95 = []byte{
-	// 3397 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x5b, 0x4d, 0x70, 0x1b, 0xd7,
-	0x91, 0xde, 0x01, 0x40, 0xfc, 0x34, 0x40, 0x02, 0x7a, 0xa6, 0x28, 0x08, 0x92, 0x2c, 0x69, 0x5c,
-	0xf6, 0x72, 0x29, 0x0b, 0x90, 0x20, 0x4b, 0x96, 0xb9, 0x6b, 0x7b, 0xf9, 0x03, 0x59, 0x92, 0xf9,
-	0xb7, 0x03, 0x92, 0x92, 0x77, 0xbd, 0x1a, 0x0f, 0x31, 0x4f, 0xe0, 0xac, 0x80, 0x19, 0x68, 0xe6,
-	0x81, 0x14, 0xe5, 0x52, 0x6d, 0xd5, 0x56, 0x79, 0xbd, 0xb5, 0x5b, 0xe5, 0xad, 0xad, 0xcd, 0x25,
-	0xf1, 0x2d, 0xc7, 0x54, 0x8e, 0xb9, 0xe4, 0xef, 0x92, 0x93, 0x53, 0xc9, 0x29, 0xa9, 0x24, 0xc7,
-	0x5c, 0x72, 0xc9, 0xcf, 0x35, 0x97, 0x54, 0x0e, 0xa9, 0xf7, 0x33, 0x7f, 0xc0, 0x50, 0x03, 0x50,
-	0x74, 0x92, 0x13, 0x67, 0xfa, 0xbd, 0xfe, 0xba, 0x5f, 0xbf, 0x7e, 0xdd, 0xfd, 0x7a, 0x40, 0xb8,
-	0xb1, 0xd3, 0x37, 0x3a, 0x7a, 0x6d, 0x47, 0x7b, 0x8a, 0x3b, 0x35, 0x1b, 0x77, 0x2d, 0x82, 0x6b,
-	0xf8, 0x09, 0x6e, 0xf5, 0x89, 0x61, 0x99, 0xb5, 0xbd, 0xba, 0xa0, 0xa9, 0x1e, 0xad, 0xda, 0xb3,
-	0x2d, 0x62, 0xa1, 0xf3, 0x8c, 0xaf, 0xca, 0xf8, 0xaa, 0x7c, 0x4e, 0xd5, 0x9f, 0xb3, 0x57, 0xaf,
-	0x9c, 0x0f, 0x02, 0x3b, 0xb8, 0xbb, 0x87, 0x6d, 0xf1, 0x87, 0x23, 0x54, 0xce, 0xb6, 0x2d, 0xab,
-	0xdd, 0xc1, 0x35, 0xad, 0x67, 0xd4, 0x34, 0xd3, 0xb4, 0x88, 0x46, 0x59, 0x1d, 0x31, 0xfa, 0x8a,
-	0x18, 0xed, 0x58, 0x66, 0xdb, 0xee, 0x9b, 0xa6, 0x61, 0xb6, 0x6b, 0x56, 0x0f, 0xdb, 0xa1, 0x49,
-	0x2f, 0x8b, 0x49, 0xec, 0x6d, 0xa7, 0xff, 0xb0, 0xa6, 0xf7, 0xf9, 0x04, 0x31, 0x7e, 0x7e, 0x70,
-	0x9c, 0x18, 0x5d, 0xec, 0x10, 0xad, 0xdb, 0x3b, 0x0c, 0x60, 0xdf, 0xd6, 0x7a, 0x3d, 0x6c, 0xbb,
-	0x02, 0x4e, 0x89, 0x71, 0xbb, 0xd7, 0xaa, 0x39, 0x44, 0x23, 0x7d, 0x31, 0x20, 0xff, 0x36, 0x01,
-	0xe9, 0x85, 0x16, 0x15, 0x85, 0xd6, 0x60, 0xaa, 0x65, 0x75, 0xbb, 0x9a, 0xa9, 0xab, 0xba, 0xd1,
-	0xc6, 0x0e, 0x29, 0x4b, 0x17, 0xa4, 0xd9, 0x7c, 0xfd, 0x6f, 0xab, 0x31, 0x26, 0xaa, 0x2e, 0xb3,
-	0xe9, 0xca, 0xa4, 0x60, 0xe7, 0xaf, 0xa8, 0x09, 0x27, 0x0c, 0xb3, 0xd7, 0x27, 0xaa, 0x6d, 0x59,
-	0xc4, 0x85, 0x4c, 0x8c, 0x07, 0x59, 0x64, 0x08, 0x8a, 0x65, 0x11, 0x01, 0x7a, 0x0d, 0x32, 0x74,
-	0xed, 0x56, 0x9f, 0x94, 0xd3, 0x0c, 0xea, 0x74, 0x95, 0x2f, 0xad, 0xea, 0x2e, 0xbd, 0xba, 0x2c,
-	0x6c, 0xa7, 0xb8, 0x33, 0xd1, 0x05, 0x28, 0xe8, 0x96, 0x6a, 0x5a, 0x44, 0x6d, 0x69, 0xad, 0x5d,
-	0x5c, 0xce, 0x5c, 0x90, 0x66, 0xb3, 0x0a, 0xe8, 0xd6, 0x9a, 0x45, 0x96, 0x28, 0x05, 0x21, 0x48,
-	0x39, 0x5a, 0x87, 0x94, 0x73, 0x17, 0xa4, 0xd9, 0x82, 0xc2, 0x9e, 0x51, 0x03, 0xb2, 0xbd, 0x8e,
-	0x46, 0x1e, 0x5a, 0x76, 0xb7, 0x0c, 0x4c, 0xd6, 0xdf, 0xc5, 0xaa, 0xbd, 0x21, 0x18, 0x14, 0x8f,
-	0xf5, 0x6e, 0x2a, 0x9b, 0x2c, 0xa5, 0xef, 0xa6, 0xb2, 0xd9, 0x52, 0x4e, 0xfe, 0x43, 0x12, 0x32,
-	0x4b, 0xdc, 0x48, 0xe8, 0x2c, 0xe4, 0x34, 0xbb, 0xdd, 0xef, 0x62, 0x93, 0x38, 0x65, 0xe9, 0x42,
-	0x72, 0x36, 0xa7, 0xf8, 0x04, 0xf4, 0x18, 0x4e, 0x62, 0x73, 0xcf, 0xb0, 0x2d, 0x93, 0xbe, 0xab,
-	0x7b, 0x9a, 0x6d, 0x68, 0x3b, 0x1d, 0xec, 0x94, 0x13, 0x17, 0x92, 0xb3, 0xf9, 0xfa, 0x3f, 0xc4,
-	0x6a, 0x22, 0xc4, 0x54, 0x1b, 0x3e, 0xca, 0xb6, 0x00, 0x51, 0xa6, 0xf1, 0x30, 0xd1, 0x41, 0x17,
-	0xa1, 0x60, 0xf5, 0x09, 0xdd, 0xb0, 0x87, 0x06, 0x95, 0x94, 0x64, 0x3a, 0xe5, 0x39, 0xed, 0x16,
-	0x25, 0xa1, 0xcb, 0x80, 0xc4, 0x14, 0xdd, 0xb0, 0x71, 0x8b, 0x58, 0xb6, 0x81, 0x9d, 0x72, 0x8a,
-	0x4d, 0x3c, 0xc1, 0x47, 0x96, 0xfd, 0x81, 0x00, 0x62, 0x4f, 0x23, 0xbb, 0x4e, 0x39, 0x13, 0x44,
-	0xdc, 0xa0, 0xa4, 0x90, 0x91, 0x27, 0x8e, 0x6c, 0x64, 0x74, 0x09, 0x4e, 0xec, 0x5b, 0xf6, 0x23,
-	0xc3, 0x6c, 0x7b, 0x9a, 0x1d, 0x30, 0x07, 0xc9, 0x29, 0x25, 0x31, 0xe0, 0x2a, 0x76, 0x80, 0xde,
-	0x80, 0x19, 0xa1, 0x96, 0x69, 0xe9, 0x58, 0xed, 0xd9, 0xf4, 0x3c, 0x12, 0xba, 0x92, 0x2c, 0x53,
-	0x70, 0x9a, 0x8f, 0xae, 0x59, 0x3a, 0xde, 0xf0, 0xc6, 0x2a, 0xef, 0xc2, 0x4b, 0x11, 0xb6, 0xa4,
-	0x9e, 0x63, 0x6a, 0x5d, 0xcc, 0xce, 0x4a, 0x4e, 0x61, 0xcf, 0x68, 0x1a, 0x26, 0xf6, 0xb4, 0x4e,
-	0x1f, 0x33, 0x6f, 0xcf, 0x29, 0xfc, 0x45, 0xfe, 0x8a, 0x04, 0x59, 0x57, 0x75, 0xa4, 0x00, 0x04,
-	0xe4, 0x4a, 0x6c, 0x53, 0xeb, 0x23, 0xaf, 0xbc, 0x2a, 0xf4, 0x3a, 0x50, 0x02, 0x28, 0x95, 0x37,
-	0x20, 0xeb, 0xd2, 0xc7, 0x50, 0xeb, 0x8b, 0x04, 0xe4, 0x7c, 0xdb, 0xbc, 0x0b, 0x13, 0x7c, 0xf7,
-	0xb9, 0x4a, 0xf1, 0x9b, 0x41, 0x1d, 0x83, 0x5a, 0x4a, 0xe1, 0x7c, 0x68, 0x03, 0xf2, 0x41, 0xdf,
-	0xe0, 0xee, 0x5a, 0x1d, 0xe1, 0xbc, 0x0b, 0x0d, 0x18, 0x56, 0x10, 0x02, 0xdd, 0x86, 0xac, 0x73,
-	0xd0, 0xed, 0x18, 0xe6, 0x23, 0xee, 0x93, 0xf9, 0xfa, 0xeb, 0xb1, 0x70, 0x4d, 0xce, 0xc0, 0xc0,
-	0x3c, 0x6e, 0x74, 0x1f, 0x8a, 0x83, 0x3b, 0xce, 0x7d, 0xae, 0x16, 0x0b, 0x18, 0x76, 0x06, 0x65,
-	0xca, 0x0c, 0xbd, 0xdf, 0x4d, 0x65, 0x53, 0xa5, 0x09, 0xf9, 0x26, 0x14, 0x02, 0xf3, 0xc6, 0xd9,
-	0x84, 0x1f, 0x4b, 0x30, 0x15, 0x16, 0x81, 0x56, 0x23, 0x3c, 0xe4, 0xf2, 0x38, 0x7a, 0x86, 0x9c,
-	0x03, 0x5d, 0x81, 0x89, 0x2e, 0x8d, 0x87, 0x22, 0x02, 0x57, 0x86, 0xc2, 0xe6, 0xa6, 0x9b, 0x52,
-	0x14, 0x3e, 0x11, 0xbd, 0x05, 0xb9, 0xbe, 0x69, 0x3c, 0x51, 0xbb, 0x96, 0x8e, 0xcb, 0x49, 0xc6,
-	0x75, 0x76, 0x88, 0x6b, 0xeb, 0x8e, 0x49, 0xae, 0xd5, 0xb7, 0xe9, 0x22, 0x94, 0x2c, 0x9d, 0xbe,
-	0x6a, 0xe9, 0x58, 0xfe, 0x8d, 0x04, 0x59, 0xd7, 0x31, 0x22, 0xad, 0xf0, 0x2e, 0xa4, 0x8f, 0x96,
-	0x10, 0x04, 0x1b, 0x7a, 0x05, 0x26, 0x0d, 0x47, 0x24, 0x73, 0x7a, 0x0e, 0xcb, 0x29, 0x16, 0xd3,
-	0x0b, 0x86, 0xd3, 0xf0, 0x68, 0x51, 0xfb, 0x9d, 0x3e, 0xae, 0xfd, 0x4e, 0x96, 0x52, 0x77, 0x53,
-	0xd9, 0x89, 0x52, 0x5a, 0xd6, 0x61, 0x32, 0xe4, 0xbd, 0x5f, 0xca, 0x82, 0xe5, 0xaf, 0x4a, 0x90,
-	0x0f, 0x78, 0x75, 0xa4, 0x90, 0x19, 0x48, 0x13, 0xcd, 0x6e, 0x63, 0x22, 0x9c, 0x4b, 0xbc, 0x45,
-	0xd9, 0x21, 0x75, 0x8c, 0x76, 0x90, 0xff, 0x1e, 0xd2, 0x22, 0x3d, 0x23, 0x48, 0xed, 0x6a, 0xce,
-	0xae, 0xab, 0x15, 0x7d, 0x46, 0xe7, 0x00, 0x1c, 0xe3, 0x29, 0x56, 0x77, 0x0e, 0x08, 0x0b, 0x08,
-	0xd2, 0x6c, 0x52, 0xc9, 0x51, 0xca, 0x22, 0x25, 0xc8, 0xbf, 0x4c, 0xc3, 0x0c, 0xdf, 0x33, 0xac,
-	0xf3, 0x4a, 0x64, 0x15, 0x13, 0x4d, 0xd7, 0x88, 0x46, 0xd7, 0x43, 0x83, 0x37, 0xb6, 0x05, 0x9e,
-	0x78, 0x43, 0x0d, 0x28, 0x3d, 0xee, 0xe3, 0x3e, 0xd6, 0x55, 0xaf, 0x0e, 0x1a, 0xc1, 0xad, 0x8b,
-	0x9c, 0xc7, 0x23, 0xa0, 0x0d, 0x98, 0xe1, 0x80, 0xaa, 0x43, 0x34, 0x9b, 0x04, 0xc0, 0x92, 0xb1,
-	0x60, 0xd3, 0x9c, 0xb3, 0x49, 0x19, 0x7d, 0xc4, 0xfb, 0x50, 0x11, 0x88, 0x2d, 0xab, 0xdb, 0xeb,
-	0x60, 0x12, 0x52, 0x31, 0x15, 0x8b, 0x5a, 0xe6, 0xdc, 0x4b, 0x2e, 0xb3, 0x8f, 0xfc, 0x01, 0x9c,
-	0xe1, 0xc5, 0xd4, 0x43, 0x4c, 0x5a, 0xbb, 0x43, 0x0a, 0x4f, 0xc4, 0x43, 0x33, 0xf6, 0x5b, 0x94,
-	0x7b, 0x40, 0x69, 0x0d, 0xce, 0x07, 0xa1, 0xa3, 0x34, 0x4f, 0xc7, 0xc2, 0x9f, 0xf5, 0xe1, 0x23,
-	0xb4, 0xdf, 0x86, 0xd3, 0x9e, 0x57, 0x0d, 0xe9, 0x9e, 0x89, 0x05, 0x3f, 0xe5, 0x31, 0x0f, 0xa8,
-	0xfe, 0x00, 0xce, 0xf9, 0xb8, 0x51, 0x8a, 0x67, 0x63, 0xb1, 0xcf, 0x78, 0x00, 0x11, 0x7a, 0xff,
-	0x2b, 0x9c, 0x13, 0x95, 0x42, 0xbf, 0xd7, 0xb1, 0x34, 0x7d, 0x48, 0xf7, 0x5c, 0x2c, 0x7e, 0x85,
-	0x03, 0x6c, 0x31, 0xfe, 0x01, 0xf5, 0x31, 0x5c, 0x0c, 0xc3, 0x47, 0x2d, 0x01, 0x62, 0x45, 0xbc,
-	0x1c, 0x14, 0x31, 0xbc, 0x0a, 0xf9, 0x47, 0x69, 0x28, 0xf0, 0x93, 0xa5, 0x60, 0xa7, 0xdf, 0x21,
-	0x68, 0x6d, 0xa0, 0xd2, 0xe3, 0x49, 0xfa, 0x52, 0x6c, 0x30, 0x58, 0xf7, 0x4a, 0xc1, 0x70, 0x59,
-	0xf8, 0x11, 0x4c, 0x07, 0xf0, 0x54, 0x2f, 0x5b, 0xc3, 0x88, 0xc9, 0x9f, 0xe3, 0x8a, 0xe8, 0xa6,
-	0x20, 0x1f, 0xba, 0xe9, 0x66, 0xee, 0x7b, 0x50, 0x14, 0x12, 0x3c, 0xf0, 0xc2, 0x91, 0xc0, 0xa7,
-	0xac, 0xe0, 0xab, 0x83, 0xd4, 0xc8, 0x8a, 0x96, 0x97, 0x19, 0x57, 0x46, 0xc4, 0xf6, 0xa2, 0x7f,
-	0x54, 0x0d, 0xfc, 0x6f, 0x70, 0x7a, 0x40, 0xc0, 0x81, 0xbf, 0x86, 0xfc, 0x91, 0xd6, 0x70, 0x2a,
-	0x2c, 0xe5, 0xc0, 0x5b, 0xcc, 0x19, 0xc8, 0xe1, 0x27, 0x06, 0x51, 0x5b, 0x34, 0x63, 0xd3, 0x68,
-	0x33, 0xa1, 0x64, 0x29, 0x61, 0x89, 0x26, 0x0c, 0x1a, 0x86, 0x89, 0x6e, 0xd1, 0xfb, 0x98, 0xb6,
-	0xcf, 0x02, 0x46, 0x41, 0xc9, 0x71, 0x8a, 0xa2, 0xed, 0xa3, 0x15, 0x98, 0x14, 0xc3, 0x22, 0x4f,
-	0xa5, 0xc7, 0xcb, 0x53, 0x05, 0xce, 0x2d, 0xf2, 0x00, 0x17, 0x86, 0x6d, 0x9b, 0x09, 0xcb, 0x78,
-	0xc2, 0xb0, 0x6d, 0xfb, 0xc2, 0xe8, 0xb0, 0x10, 0x96, 0x1d, 0x5f, 0x18, 0xb6, 0x6d, 0x21, 0xec,
-	0x21, 0x20, 0x3f, 0x0a, 0x74, 0x45, 0xf2, 0x10, 0x47, 0xf3, 0xcd, 0x58, 0xc8, 0xe8, 0xdc, 0xa3,
-	0x9c, 0xf0, 0x26, 0xb9, 0xa4, 0xbb, 0xa9, 0xac, 0x54, 0x4a, 0xc8, 0xff, 0x95, 0x00, 0xf0, 0x0f,
-	0x02, 0xcd, 0x78, 0xf4, 0x72, 0xe3, 0x66, 0x3c, 0xfa, 0xfc, 0x67, 0xaa, 0x6e, 0x2a, 0x90, 0x6d,
-	0x59, 0x26, 0x61, 0xf7, 0x47, 0xbe, 0x9d, 0xde, 0x7b, 0x54, 0xc6, 0xcf, 0x1c, 0x6f, 0xe5, 0x93,
-	0x2e, 0x65, 0xe4, 0xcf, 0x24, 0x48, 0x6d, 0xda, 0x18, 0xa3, 0x77, 0x20, 0x45, 0x2f, 0xf9, 0xa2,
-	0x61, 0x30, 0x37, 0x7a, 0xb5, 0xaf, 0x30, 0x3e, 0x74, 0x0b, 0xb2, 0xad, 0x5d, 0xa3, 0xa3, 0xdb,
-	0xd8, 0x14, 0xc1, 0x68, 0x1c, 0x0c, 0x8f, 0x57, 0xee, 0x43, 0x71, 0xe0, 0x48, 0x46, 0xee, 0xcf,
-	0x6d, 0xc8, 0x13, 0x1b, 0x63, 0xd7, 0xf9, 0x92, 0xe3, 0x6d, 0x12, 0x50, 0x5e, 0xfe, 0x7c, 0x37,
-	0x95, 0x4d, 0x94, 0x92, 0xf2, 0xe7, 0x12, 0x4c, 0x86, 0x8e, 0x68, 0xa4, 0xd4, 0xbf, 0x54, 0x75,
-	0x76, 0x19, 0x8a, 0x0d, 0x97, 0x69, 0xc3, 0xea, 0x18, 0xad, 0x03, 0xea, 0x3a, 0x3d, 0xdb, 0xb0,
-	0x6c, 0x83, 0x1c, 0x30, 0x15, 0x27, 0x14, 0xef, 0x5d, 0xbe, 0x02, 0x88, 0xa7, 0x09, 0x87, 0xb5,
-	0x46, 0x46, 0xe0, 0xf8, 0x24, 0x09, 0x53, 0xe2, 0x14, 0x29, 0xf8, 0x71, 0xdf, 0x75, 0x60, 0xd3,
-	0x21, 0x9a, 0xd9, 0xc2, 0x6a, 0xa0, 0x4c, 0x2d, 0xb8, 0xc4, 0x35, 0x5a, 0xae, 0xce, 0xc1, 0x09,
-	0xe7, 0x91, 0xd1, 0xe3, 0x4d, 0x19, 0xb5, 0x63, 0x59, 0x8f, 0xfa, 0xbc, 0xf4, 0xca, 0x2a, 0x45,
-	0x3a, 0xc0, 0xe4, 0xaf, 0x30, 0x32, 0x8d, 0x18, 0x1a, 0x3b, 0xa0, 0x47, 0x0d, 0x4f, 0x9c, 0x5b,
-	0x44, 0x8c, 0x7f, 0x81, 0x92, 0x1f, 0x31, 0x7a, 0x6c, 0x85, 0xe2, 0x7c, 0x5c, 0x19, 0x31, 0x5e,
-	0x78, 0xb6, 0x54, 0x8a, 0x78, 0xc0, 0xb8, 0x18, 0xa6, 0x6d, 0x6e, 0x40, 0xb1, 0x32, 0x21, 0x80,
-	0xc7, 0xb8, 0x6b, 0xb1, 0x02, 0x86, 0xad, 0xaf, 0x20, 0x7b, 0x88, 0xc6, 0x5d, 0x8f, 0x5f, 0x3c,
-	0xc5, 0x45, 0xe4, 0x31, 0x64, 0x56, 0xac, 0x36, 0x8b, 0x4a, 0x7e, 0x04, 0x92, 0x8e, 0x16, 0x81,
-	0x5e, 0x85, 0xa9, 0xdd, 0x7e, 0x57, 0x33, 0x55, 0x1b, 0x6b, 0x3a, 0x0b, 0x41, 0x09, 0xb6, 0x31,
-	0x93, 0x8c, 0xaa, 0x08, 0xa2, 0xfc, 0xbf, 0x49, 0xd7, 0xb9, 0xb0, 0x82, 0x9d, 0x9e, 0x65, 0x3a,
-	0x18, 0x35, 0x20, 0xcd, 0xd5, 0x15, 0xb2, 0xe3, 0x2f, 0xad, 0xc1, 0xe2, 0x44, 0x11, 0xcc, 0xd4,
-	0x85, 0x98, 0xf9, 0x74, 0x55, 0xa0, 0x71, 0x05, 0x0a, 0x9c, 0x28, 0x2a, 0x99, 0x39, 0x48, 0xf3,
-	0x76, 0xa6, 0x38, 0xc4, 0xc8, 0x2d, 0x93, 0xec, 0x5e, 0xab, 0xda, 0x64, 0x23, 0x8a, 0x98, 0x81,
-	0x34, 0xc8, 0x3b, 0xd8, 0xde, 0xc3, 0xb6, 0xda, 0xb1, 0xda, 0xbc, 0x6b, 0x95, 0xaf, 0xff, 0xe3,
-	0xa8, 0xf9, 0xc1, 0x5d, 0x5e, 0xb5, 0xc9, 0x30, 0x56, 0xac, 0xb6, 0xd3, 0x30, 0x89, 0x7d, 0xa0,
-	0x80, 0xe3, 0x11, 0x50, 0x19, 0x32, 0x5d, 0xec, 0x38, 0x5a, 0x1b, 0xb3, 0x88, 0x9c, 0x53, 0xdc,
-	0xd7, 0x4a, 0x1b, 0x8a, 0x03, 0x8c, 0xa8, 0x04, 0xc9, 0x47, 0xf8, 0x40, 0x9c, 0x0c, 0xfa, 0x88,
-	0xde, 0x09, 0xf6, 0x06, 0xf2, 0xf5, 0xd9, 0x58, 0xdd, 0xc4, 0x76, 0x8b, 0x2e, 0xc2, 0x7c, 0xe2,
-	0xa6, 0x24, 0x6b, 0xee, 0x59, 0xe4, 0xd5, 0x72, 0x1b, 0xcb, 0xeb, 0x30, 0xc1, 0xee, 0xe7, 0x28,
-	0x0f, 0x99, 0xad, 0xb5, 0xf7, 0xd7, 0xd6, 0xef, 0xad, 0x95, 0xfe, 0x06, 0x15, 0x21, 0xbf, 0xb4,
-	0xb0, 0x74, 0xbb, 0xa1, 0x2e, 0xdd, 0x6e, 0x2c, 0xbd, 0x5f, 0x92, 0x10, 0x40, 0xfa, 0x9f, 0xb6,
-	0x1a, 0x5b, 0x8d, 0xe5, 0x52, 0x02, 0x4d, 0x42, 0xae, 0x71, 0xbf, 0xb1, 0xb4, 0xb5, 0x79, 0x67,
-	0xed, 0xbd, 0x52, 0x92, 0xbe, 0x2e, 0xad, 0xaf, 0x6e, 0xac, 0x34, 0x36, 0x1b, 0xcb, 0xa5, 0x94,
-	0xfc, 0x7f, 0x09, 0x28, 0x0b, 0xab, 0xac, 0xbb, 0x9d, 0x6c, 0xef, 0xce, 0xf6, 0x3e, 0x4c, 0x38,
-	0x54, 0x2c, 0x5b, 0xd7, 0x54, 0xfd, 0xfa, 0xe8, 0xe7, 0x89, 0x69, 0x5b, 0xe5, 0xad, 0x04, 0x8e,
-	0x31, 0x7c, 0xea, 0x13, 0x2f, 0x72, 0xea, 0x5f, 0x07, 0x24, 0x4a, 0x1c, 0x87, 0xd8, 0x58, 0xeb,
-	0xf2, 0xc8, 0x94, 0xe4, 0x5d, 0x42, 0x3e, 0xd2, 0x64, 0x03, 0x2c, 0x3a, 0xf1, 0xd9, 0xb4, 0x46,
-	0x09, 0xce, 0x4e, 0x79, 0xb3, 0xb1, 0x6d, 0xfb, 0xb3, 0xe5, 0x39, 0x98, 0xbe, 0xa7, 0x19, 0xc4,
-	0x5b, 0x8c, 0x1b, 0x08, 0x23, 0xae, 0xe9, 0xf2, 0x27, 0x09, 0x98, 0x79, 0x0f, 0x93, 0x90, 0xd7,
-	0x8f, 0x13, 0x37, 0x8f, 0xd7, 0x2a, 0x4c, 0x64, 0xc7, 0x30, 0xb1, 0xca, 0x4d, 0x20, 0x22, 0x70,
-	0x81, 0x13, 0x9b, 0x8c, 0x16, 0x9e, 0x84, 0x6d, 0xdb, 0x2b, 0x48, 0xdc, 0x49, 0xd8, 0xb6, 0x51,
-	0x15, 0x5e, 0x12, 0x93, 0x42, 0xb7, 0x8b, 0x09, 0xde, 0x1e, 0xe6, 0x43, 0x7e, 0xe5, 0xe4, 0xc8,
-	0x3f, 0x4c, 0xc0, 0xe9, 0xad, 0x9e, 0xae, 0x11, 0xfc, 0x57, 0x62, 0x0a, 0xc5, 0x43, 0x13, 0x21,
-	0x27, 0x79, 0x94, 0x00, 0x26, 0x30, 0x45, 0x84, 0x3a, 0x2c, 0x1b, 0xa4, 0x8e, 0x35, 0x1b, 0xc8,
-	0xff, 0x2d, 0xc1, 0xa9, 0x5b, 0x86, 0xa9, 0xaf, 0x1a, 0x8e, 0x63, 0x98, 0xed, 0xc5, 0x8e, 0xb5,
-	0xe3, 0x8c, 0x65, 0xc9, 0xbb, 0x50, 0xd8, 0xe9, 0x58, 0x3b, 0xc2, 0x8e, 0xee, 0x9d, 0x70, 0x64,
-	0x43, 0xe6, 0x29, 0x33, 0x7f, 0x76, 0xe4, 0x3e, 0x94, 0x87, 0x75, 0x11, 0xd9, 0xe1, 0x03, 0x98,
-	0xee, 0x72, 0xba, 0xfa, 0x22, 0xf2, 0x50, 0xd7, 0x07, 0x77, 0xc5, 0xfe, 0x51, 0x82, 0x53, 0x8b,
-	0x1a, 0x69, 0xed, 0x72, 0xa7, 0x1a, 0xdf, 0x06, 0x1f, 0x42, 0xd6, 0xe6, 0xf3, 0x5d, 0x7d, 0xe2,
-	0xd3, 0xc3, 0x21, 0x02, 0xab, 0xe2, 0xaf, 0xe2, 0x21, 0x56, 0x1e, 0x40, 0xc6, 0xd5, 0xe6, 0x85,
-	0xd3, 0x33, 0x82, 0x14, 0xbb, 0xe4, 0x24, 0xf8, 0xf7, 0x2a, 0xfa, 0x2c, 0xff, 0x5e, 0x82, 0xf2,
-	0xb0, 0x36, 0xc2, 0xec, 0x1f, 0x41, 0xce, 0x16, 0xcf, 0x6e, 0x33, 0x79, 0xf1, 0x08, 0x6b, 0x13,
-	0x39, 0xd0, 0x7d, 0x50, 0x7c, 0xd0, 0xca, 0x3e, 0x64, 0x3d, 0x69, 0x2f, 0xbc, 0x3e, 0x3f, 0xaf,
-	0x27, 0xe2, 0xf2, 0xba, 0xfc, 0xef, 0x70, 0x92, 0x29, 0x4a, 0x8b, 0x92, 0xf1, 0xf7, 0x7c, 0x01,
-	0x32, 0x47, 0x74, 0x41, 0x97, 0x4f, 0xfe, 0xcf, 0x04, 0xcc, 0x0c, 0x6a, 0x20, 0x0c, 0xf1, 0x60,
-	0xd8, 0xec, 0x23, 0xba, 0xd4, 0x10, 0x56, 0xa4, 0xd1, 0xff, 0x47, 0x3a, 0x4e, 0xab, 0x47, 0x78,
-	0xd5, 0x38, 0x15, 0x96, 0xfc, 0x5d, 0x09, 0xa6, 0xde, 0xc3, 0x84, 0x5e, 0x09, 0xc7, 0xda, 0x83,
-	0xdb, 0x90, 0x7f, 0x81, 0x6f, 0xc4, 0x60, 0xfb, 0x9f, 0x87, 0xcf, 0x40, 0xae, 0xa7, 0xb5, 0xb1,
-	0xea, 0x18, 0x4f, 0x79, 0x66, 0xa7, 0xf7, 0x14, 0xad, 0x8d, 0x9b, 0xc6, 0x53, 0xd6, 0x01, 0x61,
-	0x83, 0xc4, 0x7a, 0x84, 0x4d, 0x91, 0xc9, 0xd9, 0xf4, 0x4d, 0x4a, 0x90, 0x3f, 0x95, 0xa0, 0xe8,
-	0x69, 0x2f, 0x4c, 0xba, 0x12, 0xfe, 0x9a, 0x25, 0x8d, 0x7d, 0x37, 0x0d, 0x7d, 0xc9, 0x7a, 0x0d,
-	0x8a, 0x26, 0x7e, 0x42, 0xd4, 0x80, 0x16, 0xfc, 0x2a, 0x38, 0x49, 0xc9, 0x1b, 0x9e, 0x26, 0x6f,
-	0xb3, 0xfa, 0x60, 0x49, 0xeb, 0x69, 0x3b, 0x46, 0xc7, 0x60, 0x57, 0xbb, 0x31, 0xcc, 0x29, 0xff,
-	0x20, 0x09, 0x88, 0x17, 0x9b, 0x41, 0x08, 0xa4, 0x01, 0xe2, 0x19, 0xa8, 0x15, 0xa0, 0x0a, 0x57,
-	0x89, 0xff, 0xf4, 0xc8, 0x92, 0x4d, 0x48, 0xa5, 0x13, 0xad, 0x41, 0x12, 0xea, 0xc2, 0x4c, 0xa0,
-	0x1f, 0x1b, 0x14, 0xc3, 0xf7, 0xf4, 0xc6, 0xe8, 0xd5, 0x60, 0x48, 0xd4, 0x49, 0x1c, 0x45, 0x46,
-	0x1b, 0x30, 0xa3, 0xe3, 0x9e, 0x8d, 0x5b, 0x1a, 0xc1, 0xba, 0xaa, 0xf5, 0x0c, 0x75, 0x0f, 0xdb,
-	0x8e, 0x61, 0x99, 0x5e, 0x03, 0x3f, 0x28, 0x4e, 0xfc, 0x68, 0xa3, 0x89, 0xbb, 0xdb, 0xd8, 0x56,
-	0xa6, 0x7d, 0xce, 0x85, 0x9e, 0xb1, 0xcd, 0xf9, 0xd0, 0x22, 0x14, 0x3b, 0xd6, 0x7e, 0x08, 0x2a,
-	0x15, 0x0b, 0x35, 0xd9, 0xb1, 0xf6, 0x03, 0x18, 0xcb, 0x50, 0xda, 0x35, 0xda, 0xbb, 0x21, 0x90,
-	0x89, 0x58, 0x90, 0x29, 0xca, 0xe3, 0xa3, 0xc8, 0x0e, 0x4c, 0x71, 0x9f, 0xbe, 0xd5, 0x37, 0x59,
-	0x3d, 0x21, 0x6b, 0x91, 0x75, 0x3c, 0x40, 0xba, 0x79, 0x7b, 0xa1, 0x7e, 0xfd, 0x46, 0x49, 0x42,
-	0x59, 0x48, 0x35, 0x6f, 0x2f, 0x5c, 0x2d, 0x25, 0x50, 0x06, 0x92, 0xab, 0xcb, 0xd7, 0x4b, 0x49,
-	0xfa, 0xb0, 0xdd, 0x5c, 0x2f, 0xa5, 0xc4, 0xbc, 0x6b, 0x37, 0xdf, 0x28, 0x4d, 0x88, 0xe7, 0xeb,
-	0x57, 0xeb, 0xa5, 0x34, 0x05, 0x5b, 0xdd, 0x52, 0x56, 0xb7, 0x94, 0x6b, 0xa5, 0x8c, 0x7c, 0x0b,
-	0xce, 0xf1, 0x52, 0x86, 0xed, 0x36, 0x8f, 0xfc, 0x21, 0x8b, 0xbf, 0x0a, 0x53, 0x7d, 0x46, 0x55,
-	0xb1, 0x49, 0x2f, 0x80, 0x3a, 0xf3, 0x9f, 0xac, 0x32, 0xc9, 0xa9, 0x0d, 0x4e, 0x94, 0x7f, 0x22,
-	0xc1, 0xf4, 0x86, 0x68, 0x0f, 0x84, 0xf8, 0x5b, 0x00, 0xa2, 0x6d, 0xe0, 0x1f, 0xa7, 0xa5, 0xf8,
-	0xcf, 0xde, 0x11, 0x50, 0x1e, 0x51, 0xd1, 0xcc, 0x36, 0x56, 0x02, 0xb0, 0x95, 0x2d, 0x98, 0x0c,
-	0x0d, 0xa2, 0x8b, 0x50, 0xe8, 0x1a, 0xa6, 0x3a, 0xd0, 0xc0, 0xc8, 0x77, 0x0d, 0xd3, 0x9d, 0xc7,
-	0xa6, 0x68, 0x4f, 0xfc, 0x29, 0x09, 0x31, 0x45, 0x7b, 0xe2, 0x4e, 0x91, 0x37, 0xe0, 0x8c, 0x68,
-	0xef, 0x2c, 0xec, 0x38, 0x56, 0xa7, 0x4f, 0xf0, 0x86, 0x46, 0x76, 0x9b, 0xc4, 0xd6, 0x08, 0x6e,
-	0x1f, 0xc8, 0x57, 0x23, 0xb7, 0x67, 0x0a, 0x60, 0xf9, 0x4e, 0x73, 0x61, 0x65, 0x65, 0xfd, 0x5e,
-	0x63, 0xb9, 0x24, 0xd1, 0x41, 0xf7, 0x25, 0x21, 0x7f, 0x27, 0x05, 0x27, 0x86, 0xce, 0x15, 0x7a,
-	0x00, 0x45, 0x1e, 0x08, 0xd5, 0x87, 0x62, 0xeb, 0x99, 0xa1, 0x46, 0xb9, 0x4b, 0x85, 0x3d, 0x46,
-	0xdc, 0xa5, 0xa6, 0xf4, 0x10, 0x15, 0x7d, 0x2a, 0xc1, 0x05, 0x51, 0xe6, 0xf2, 0x78, 0x20, 0x76,
-	0x34, 0xe2, 0xbc, 0xbe, 0x33, 0x62, 0xe5, 0x7b, 0x88, 0xbb, 0x28, 0xe7, 0xb4, 0xe7, 0x7a, 0x53,
-	0x1f, 0xce, 0x88, 0x9a, 0x58, 0xd8, 0x38, 0xac, 0x03, 0x3f, 0xc4, 0xd7, 0x8f, 0xe4, 0x1e, 0xca,
-	0x69, 0x86, 0x1c, 0xe9, 0x84, 0xf3, 0x50, 0xa1, 0x7b, 0xbd, 0x43, 0xd3, 0xac, 0x4a, 0x2c, 0xa2,
-	0x75, 0xd4, 0xc0, 0x07, 0xca, 0x14, 0xfb, 0x40, 0x39, 0xd3, 0xd5, 0x9e, 0xb0, 0x3c, 0xbc, 0x49,
-	0xc7, 0x9b, 0xee, 0xd7, 0x4a, 0x6a, 0xbc, 0x97, 0x45, 0xfb, 0x5e, 0xd5, 0x84, 0x1b, 0xb0, 0x5f,
-	0xb7, 0xd0, 0x5b, 0x22, 0x73, 0x04, 0x76, 0xd6, 0xa7, 0x46, 0xa8, 0xae, 0x9e, 0xe3, 0x4c, 0x62,
-	0xe7, 0xce, 0x38, 0xcf, 0xf1, 0xb7, 0x9f, 0x25, 0xe0, 0x64, 0x64, 0xb4, 0x8c, 0x76, 0x20, 0xe9,
-	0xf8, 0x1c, 0xe8, 0x22, 0x14, 0x28, 0x93, 0x17, 0x02, 0x78, 0x63, 0x26, 0x4f, 0x69, 0x22, 0x00,
-	0xa0, 0x67, 0x70, 0x3e, 0xd0, 0x60, 0x3b, 0xfe, 0xdd, 0xf5, 0x3f, 0xfb, 0x1d, 0xb2, 0xc3, 0xa7,
-	0x9d, 0x7e, 0xaf, 0x67, 0xd9, 0x34, 0x2f, 0x0c, 0x37, 0x57, 0xe9, 0x7d, 0xf4, 0x94, 0x37, 0x21,
-	0xdc, 0x44, 0x95, 0x57, 0x21, 0xbf, 0x69, 0x59, 0x9d, 0x65, 0x4c, 0x34, 0xa3, 0xc3, 0xbe, 0xa9,
-	0x10, 0xcb, 0xea, 0x04, 0xb3, 0x6d, 0x96, 0x12, 0x58, 0xe1, 0x72, 0x11, 0x0a, 0x6c, 0xd0, 0x0d,
-	0xf3, 0x3c, 0x9b, 0xe7, 0x29, 0xcd, 0x8d, 0xe3, 0xbf, 0x96, 0xa0, 0x28, 0xb2, 0xb7, 0xd7, 0x23,
-	0x59, 0x17, 0x6c, 0x3a, 0x97, 0x21, 0x72, 0x70, 0xfc, 0xaf, 0x5a, 0x02, 0x7a, 0x71, 0x21, 0x01,
-	0x25, 0xc5, 0x89, 0x36, 0x74, 0xa1, 0x44, 0x96, 0x13, 0xee, 0xe8, 0xe8, 0x75, 0x40, 0x4c, 0x9a,
-	0x61, 0xee, 0x59, 0x2d, 0xcd, 0x9d, 0x25, 0xda, 0x1e, 0x74, 0xe4, 0x8e, 0x37, 0x70, 0x47, 0xa7,
-	0xa6, 0x6b, 0x59, 0xb6, 0x8d, 0x3b, 0x2c, 0xa7, 0xfa, 0x3c, 0x0e, 0x65, 0xe2, 0x35, 0xd3, 0x29,
-	0x7f, 0x82, 0xcf, 0xea, 0xdc, 0xd1, 0xeb, 0xdf, 0x4e, 0x40, 0xce, 0x73, 0x49, 0xf4, 0x99, 0x04,
-	0x19, 0xd1, 0x26, 0x42, 0xb5, 0xd1, 0xdb, 0x6c, 0xcc, 0x54, 0x95, 0x73, 0x6e, 0x99, 0x19, 0xf8,
-	0xdd, 0x64, 0xd5, 0xeb, 0x36, 0xc9, 0x57, 0xff, 0xe3, 0xa7, 0xbf, 0xfa, 0xff, 0xc4, 0x25, 0xf9,
-	0xb5, 0xda, 0x5e, 0xbd, 0xf6, 0x71, 0xa8, 0x24, 0x7a, 0x7b, 0x6e, 0xee, 0x59, 0x8d, 0x2f, 0xde,
-	0x99, 0xe7, 0x22, 0xf0, 0xbc, 0x34, 0x77, 0x45, 0x42, 0x5f, 0x93, 0x60, 0x32, 0xd4, 0xa4, 0x41,
-	0xf1, 0xde, 0x17, 0xd5, 0xd4, 0x19, 0x4f, 0x39, 0xa6, 0x93, 0xff, 0x8b, 0xcf, 0xda, 0xdc, 0xdc,
-	0xb3, 0xf9, 0xfd, 0x20, 0x2a, 0x53, 0xae, 0xfe, 0xf3, 0x24, 0xe4, 0x03, 0xc1, 0x14, 0xfd, 0x82,
-	0x57, 0xa3, 0xa1, 0xef, 0xb6, 0xf1, 0x1f, 0xb3, 0xa2, 0xdb, 0x4a, 0x95, 0xf1, 0x3a, 0x18, 0xf2,
-	0x87, 0x6c, 0x01, 0xdb, 0x68, 0xf3, 0xb9, 0xd6, 0x15, 0x9d, 0x89, 0xda, 0xc7, 0xa1, 0x0e, 0x4c,
-	0x75, 0x57, 0x73, 0x76, 0x9f, 0x0d, 0x12, 0xfd, 0xf8, 0xfa, 0x0c, 0xfd, 0x4e, 0x02, 0x34, 0xdc,
-	0xf6, 0x41, 0xf3, 0xb1, 0x3a, 0x1e, 0xda, 0x2b, 0x1a, 0x77, 0x7d, 0x8f, 0xd8, 0xfa, 0x70, 0xe5,
-	0x4b, 0x59, 0xdf, 0x7c, 0xb8, 0x87, 0x54, 0xff, 0x3c, 0x0d, 0xa7, 0x97, 0xf8, 0x77, 0xb9, 0x05,
-	0x5d, 0xb7, 0xb1, 0xe3, 0xd0, 0x10, 0xd9, 0x24, 0x96, 0xad, 0xb5, 0x31, 0xfa, 0x9e, 0x04, 0xa5,
-	0xc1, 0x5e, 0x09, 0xba, 0x39, 0xc2, 0xaf, 0xee, 0x22, 0x5b, 0x3d, 0x95, 0xb7, 0x8e, 0xc0, 0xc9,
-	0xaf, 0x3a, 0xf2, 0x35, 0x66, 0x94, 0xcb, 0xf2, 0xec, 0x21, 0x46, 0xd9, 0xa1, 0xb3, 0xe7, 0x1f,
-	0xfa, 0xec, 0xf3, 0xd2, 0x1c, 0x53, 0x7f, 0xb0, 0x4b, 0x30, 0x82, 0xfa, 0x87, 0x34, 0x4d, 0x46,
-	0x50, 0xff, 0xb0, 0x96, 0xc4, 0x88, 0xea, 0xef, 0xf8, 0xec, 0x54, 0xfd, 0x6f, 0x49, 0x30, 0x15,
-	0xbe, 0x6d, 0xa3, 0x1b, 0x63, 0x5f, 0xcf, 0xb9, 0xea, 0x6f, 0x1e, 0xf1, 0x5a, 0x1f, 0x1b, 0xca,
-	0x02, 0x8a, 0x53, 0x66, 0xaa, 0xf6, 0x17, 0x12, 0x64, 0xc4, 0x4d, 0x75, 0x84, 0xc8, 0x1a, 0xbe,
-	0x91, 0x57, 0xae, 0x8c, 0xce, 0x20, 0x34, 0xbc, 0xcf, 0x34, 0x54, 0xd0, 0xc6, 0xf3, 0x34, 0xac,
-	0x7d, 0x1c, 0xb8, 0xc2, 0xbb, 0x87, 0x24, 0x48, 0x0a, 0x1e, 0x91, 0x36, 0x97, 0x70, 0x45, 0xaa,
-	0x7f, 0x5f, 0x82, 0x42, 0x28, 0x75, 0x7f, 0x93, 0xc7, 0xbd, 0x10, 0x6d, 0xa4, 0xb8, 0x17, 0x71,
-	0x5d, 0xae, 0xc4, 0xb7, 0x57, 0x87, 0xef, 0xc9, 0xf2, 0x25, 0xb6, 0xdc, 0x57, 0xd1, 0x2b, 0x87,
-	0x2c, 0x37, 0x58, 0xc5, 0x2c, 0xda, 0x10, 0xf7, 0x0f, 0x04, 0x8b, 0xd3, 0x0a, 0x23, 0xfa, 0xdf,
-	0x0d, 0x6d, 0x8b, 0x58, 0x1b, 0xd2, 0x3f, 0x17, 0xf9, 0x64, 0x6f, 0xee, 0xd7, 0x13, 0x49, 0xa5,
-	0x71, 0xff, 0x1b, 0x89, 0xf3, 0x8b, 0x0c, 0x70, 0x91, 0x01, 0x72, 0x5e, 0xff, 0x5a, 0x5c, 0xdd,
-	0xae, 0xef, 0xa4, 0xd9, 0x6f, 0x80, 0xae, 0xfd, 0x29, 0x00, 0x00, 0xff, 0xff, 0xde, 0x45, 0x55,
-	0xfb, 0xf3, 0x30, 0x00, 0x00,
+	// 3491 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x5b, 0xcb, 0x93, 0x1b, 0x57,
+	0x57, 0xff, 0x5a, 0xd2, 0xe8, 0x71, 0xa4, 0x19, 0x69, 0x6e, 0xc6, 0x63, 0x8d, 0x6c, 0xc7, 0x76,
+	0xa7, 0xf2, 0x31, 0x8c, 0x63, 0xc9, 0x96, 0x63, 0xc7, 0x19, 0x48, 0xc2, 0x3c, 0xe4, 0x78, 0x9c,
+	0x79, 0xd1, 0x9a, 0x19, 0x3b, 0x21, 0xb8, 0xd3, 0xa3, 0xbe, 0xd6, 0x34, 0x96, 0xfa, 0xca, 0xdd,
+	0xad, 0x79, 0x38, 0xe5, 0xa2, 0x8a, 0xaa, 0x10, 0x0a, 0xaa, 0x42, 0x51, 0xb0, 0x81, 0x6c, 0x28,
+	0x8a, 0x15, 0xc5, 0x92, 0x0d, 0x05, 0x6c, 0x58, 0x85, 0x82, 0x15, 0x14, 0xf0, 0x0f, 0xb0, 0xe1,
+	0xb1, 0x65, 0x43, 0xb1, 0xa0, 0xee, 0xa3, 0x5f, 0x52, 0x8f, 0x5b, 0x1a, 0x4f, 0x80, 0xd5, 0x74,
+	0x9f, 0x7b, 0xcf, 0xef, 0x9c, 0x7b, 0xee, 0xb9, 0xe7, 0x9c, 0x7b, 0x5a, 0x03, 0xf7, 0xf6, 0xfb,
+	0x46, 0x47, 0xaf, 0xed, 0x6b, 0x2f, 0x71, 0xa7, 0x66, 0xe1, 0x2e, 0x71, 0x70, 0x0d, 0x1f, 0xe3,
+	0x56, 0xdf, 0x31, 0x88, 0x59, 0x3b, 0xac, 0x0b, 0x9a, 0xea, 0xd1, 0xaa, 0x3d, 0x8b, 0x38, 0x04,
+	0x5d, 0x65, 0x7c, 0x55, 0xc6, 0x57, 0xe5, 0x73, 0xaa, 0xfe, 0x9c, 0xc3, 0x7a, 0xe5, 0x6a, 0x10,
+	0xd8, 0xc6, 0xdd, 0x43, 0x6c, 0x89, 0x3f, 0x1c, 0xa1, 0x72, 0xb9, 0x4d, 0x48, 0xbb, 0x83, 0x6b,
+	0x5a, 0xcf, 0xa8, 0x69, 0xa6, 0x49, 0x1c, 0x8d, 0xb2, 0xda, 0x62, 0xf4, 0x1d, 0x31, 0xda, 0x21,
+	0x66, 0xdb, 0xea, 0x9b, 0xa6, 0x61, 0xb6, 0x6b, 0xa4, 0x87, 0xad, 0xd0, 0xa4, 0x39, 0x31, 0x89,
+	0xbd, 0xed, 0xf7, 0x9f, 0xd5, 0x34, 0xf3, 0x44, 0x0c, 0xbd, 0x3d, 0x38, 0xa4, 0xf7, 0x39, 0xaf,
+	0x18, 0xbf, 0x3a, 0x38, 0xee, 0x18, 0x5d, 0x6c, 0x3b, 0x5a, 0xb7, 0x77, 0x1a, 0xc0, 0x91, 0xa5,
+	0xf5, 0x7a, 0xd8, 0x72, 0x65, 0x5f, 0x14, 0xe3, 0x56, 0xaf, 0x55, 0xb3, 0x1d, 0xcd, 0xe9, 0x8b,
+	0x01, 0xf9, 0xdf, 0x13, 0x90, 0x5e, 0x6a, 0x51, 0x51, 0x68, 0x13, 0xa6, 0x5a, 0xa4, 0xdb, 0xd5,
+	0x4c, 0x5d, 0xd5, 0x8d, 0x36, 0xb6, 0x9d, 0xb2, 0x74, 0x4d, 0x9a, 0xcf, 0xd7, 0x7f, 0xa6, 0x1a,
+	0x63, 0xbd, 0xea, 0x2a, 0x9b, 0xae, 0x4c, 0x0a, 0x76, 0xfe, 0x8a, 0x9a, 0x30, 0x6d, 0x98, 0xbd,
+	0xbe, 0xa3, 0x5a, 0x84, 0x38, 0x2e, 0x64, 0x62, 0x3c, 0xc8, 0x22, 0x43, 0x50, 0x08, 0x71, 0x04,
+	0xe8, 0x1d, 0xc8, 0xd0, 0xb5, 0x93, 0xbe, 0x53, 0x4e, 0x33, 0xa8, 0xb9, 0x2a, 0x5f, 0x5a, 0xd5,
+	0x5d, 0x7a, 0x75, 0x55, 0xd8, 0x4e, 0x71, 0x67, 0xa2, 0x6b, 0x50, 0xd0, 0x89, 0x6a, 0x12, 0x47,
+	0x6d, 0x69, 0xad, 0x03, 0x5c, 0xce, 0x5c, 0x93, 0xe6, 0xb3, 0x0a, 0xe8, 0x64, 0x93, 0x38, 0x2b,
+	0x94, 0x82, 0x10, 0xa4, 0x6c, 0xad, 0xe3, 0x94, 0x73, 0xd7, 0xa4, 0xf9, 0x82, 0xc2, 0x9e, 0x51,
+	0x03, 0xb2, 0xbd, 0x8e, 0xe6, 0x3c, 0x23, 0x56, 0xb7, 0x0c, 0x4c, 0xd6, 0xcf, 0xc6, 0xaa, 0xbd,
+	0x2d, 0x18, 0x14, 0x8f, 0xf5, 0x51, 0x2a, 0x9b, 0x2c, 0xa5, 0x1f, 0xa5, 0xb2, 0xd9, 0x52, 0x4e,
+	0xfe, 0xaf, 0x24, 0x64, 0x56, 0xb8, 0x91, 0xd0, 0x65, 0xc8, 0x69, 0x56, 0xbb, 0xdf, 0xc5, 0xa6,
+	0x63, 0x97, 0xa5, 0x6b, 0xc9, 0xf9, 0x9c, 0xe2, 0x13, 0xd0, 0x0b, 0xb8, 0x80, 0xcd, 0x43, 0xc3,
+	0x22, 0x26, 0x7d, 0x57, 0x0f, 0x35, 0xcb, 0xd0, 0xf6, 0x3b, 0xd8, 0x2e, 0x27, 0xae, 0x25, 0xe7,
+	0xf3, 0xf5, 0x9f, 0x8f, 0xd5, 0x44, 0x88, 0xa9, 0x36, 0x7c, 0x94, 0x3d, 0x01, 0xa2, 0xcc, 0xe0,
+	0x61, 0xa2, 0x8d, 0xae, 0x43, 0x81, 0xf4, 0x1d, 0xba, 0x61, 0xcf, 0x0c, 0x2a, 0x29, 0xc9, 0x74,
+	0xca, 0x73, 0xda, 0x03, 0x4a, 0x42, 0x37, 0x01, 0x89, 0x29, 0xba, 0x61, 0xe1, 0x96, 0x43, 0x2c,
+	0x03, 0xdb, 0xe5, 0x14, 0x9b, 0x38, 0xcd, 0x47, 0x56, 0xfd, 0x81, 0x00, 0x62, 0x4f, 0x73, 0x0e,
+	0xec, 0x72, 0x26, 0x88, 0xb8, 0x4d, 0x49, 0x21, 0x23, 0x4f, 0x9c, 0xd9, 0xc8, 0xe8, 0x06, 0x4c,
+	0x1f, 0x11, 0xeb, 0xb9, 0x61, 0xb6, 0x3d, 0xcd, 0x4e, 0x98, 0x83, 0xe4, 0x94, 0x92, 0x18, 0x70,
+	0x15, 0x3b, 0x41, 0xef, 0xc3, 0xac, 0x50, 0xcb, 0x24, 0x3a, 0x56, 0x7b, 0x16, 0x3d, 0xaa, 0x0e,
+	0x5d, 0x49, 0x96, 0x29, 0x38, 0xc3, 0x47, 0x37, 0x89, 0x8e, 0xb7, 0xbd, 0xb1, 0xca, 0x27, 0xf0,
+	0x56, 0x84, 0x2d, 0xa9, 0xe7, 0x98, 0x5a, 0x17, 0xb3, 0xb3, 0x92, 0x53, 0xd8, 0x33, 0x9a, 0x81,
+	0x89, 0x43, 0xad, 0xd3, 0xc7, 0xcc, 0xdb, 0x73, 0x0a, 0x7f, 0x91, 0x7f, 0x4f, 0x82, 0xac, 0xab,
+	0x3a, 0x52, 0x00, 0x02, 0x72, 0x25, 0xb6, 0xa9, 0xf5, 0x91, 0x57, 0x5e, 0x15, 0x7a, 0x9d, 0x28,
+	0x01, 0x94, 0xca, 0xfb, 0x90, 0x75, 0xe9, 0x63, 0xa8, 0xf5, 0x43, 0x02, 0x72, 0xbe, 0x6d, 0x3e,
+	0x81, 0x09, 0xbe, 0xfb, 0x5c, 0xa5, 0xf8, 0xcd, 0xa0, 0x8e, 0x41, 0x2d, 0xa5, 0x70, 0x3e, 0xb4,
+	0x0d, 0xf9, 0xa0, 0x6f, 0x70, 0x77, 0xad, 0x8e, 0x70, 0xde, 0x85, 0x06, 0x0c, 0x2b, 0x08, 0x81,
+	0x1e, 0x42, 0xd6, 0x3e, 0xe9, 0x76, 0x0c, 0xf3, 0x39, 0xf7, 0xc9, 0x7c, 0xfd, 0xbd, 0x58, 0xb8,
+	0x26, 0x67, 0x60, 0x60, 0x1e, 0x37, 0x7a, 0x02, 0xc5, 0xc1, 0x1d, 0xe7, 0x3e, 0x57, 0x8b, 0x05,
+	0x0c, 0x3b, 0x83, 0x32, 0x65, 0x86, 0xde, 0x1f, 0xa5, 0xb2, 0xa9, 0xd2, 0x84, 0x7c, 0x1f, 0x0a,
+	0x81, 0x79, 0xe3, 0x6c, 0xc2, 0xdf, 0x49, 0x30, 0x15, 0x16, 0x81, 0x36, 0x22, 0x3c, 0xe4, 0xe6,
+	0x38, 0x7a, 0x86, 0x9c, 0x03, 0xdd, 0x82, 0x89, 0x2e, 0x8d, 0x87, 0x22, 0x02, 0x57, 0x86, 0xc2,
+	0xe6, 0x8e, 0x9b, 0x52, 0x14, 0x3e, 0x11, 0x7d, 0x08, 0xb9, 0xbe, 0x69, 0x1c, 0xab, 0x5d, 0xa2,
+	0xe3, 0x72, 0x92, 0x71, 0x5d, 0x1e, 0xe2, 0xda, 0x5d, 0x33, 0x9d, 0x3b, 0xf5, 0x3d, 0xba, 0x08,
+	0x25, 0x4b, 0xa7, 0x6f, 0x10, 0x1d, 0xcb, 0xff, 0x26, 0x41, 0xd6, 0x75, 0x8c, 0x48, 0x2b, 0x7c,
+	0x02, 0xe9, 0xb3, 0x25, 0x04, 0xc1, 0x86, 0xde, 0x81, 0x49, 0xc3, 0x16, 0x79, 0x9e, 0x9e, 0xc3,
+	0x72, 0x8a, 0xc5, 0xf4, 0x82, 0x61, 0x37, 0x3c, 0x5a, 0xd4, 0x7e, 0xa7, 0xcf, 0x6b, 0xbf, 0x93,
+	0xa5, 0xd4, 0xa3, 0x54, 0x76, 0xa2, 0x94, 0x96, 0x75, 0x98, 0x0c, 0x79, 0xef, 0x8f, 0xb2, 0x60,
+	0xf9, 0xf7, 0x25, 0xc8, 0x07, 0xbc, 0x3a, 0x52, 0xc8, 0x2c, 0xa4, 0x1d, 0xcd, 0x6a, 0x63, 0x47,
+	0x38, 0x97, 0x78, 0x8b, 0xb2, 0x43, 0xea, 0x1c, 0xed, 0x20, 0xff, 0x1c, 0xa4, 0x45, 0x7a, 0x46,
+	0x90, 0x3a, 0xd0, 0xec, 0x03, 0x57, 0x2b, 0xfa, 0x8c, 0xae, 0x00, 0xd8, 0xc6, 0x4b, 0xac, 0xee,
+	0x9f, 0x38, 0x2c, 0x20, 0x48, 0xf3, 0x49, 0x25, 0x47, 0x29, 0xcb, 0x94, 0x20, 0xff, 0x71, 0x06,
+	0x66, 0xf9, 0x9e, 0x61, 0x9d, 0x57, 0x22, 0x1b, 0xd8, 0xd1, 0x74, 0xcd, 0xd1, 0xe8, 0x7a, 0x68,
+	0xf0, 0xc6, 0x96, 0xc0, 0x13, 0x6f, 0xa8, 0x01, 0xa5, 0x17, 0x7d, 0xdc, 0xc7, 0xba, 0xea, 0xd5,
+	0x41, 0x23, 0xb8, 0x75, 0x91, 0xf3, 0x78, 0x04, 0xb4, 0x0d, 0xb3, 0x1c, 0x50, 0xb5, 0x1d, 0xcd,
+	0x72, 0x02, 0x60, 0xc9, 0x58, 0xb0, 0x19, 0xce, 0xd9, 0xa4, 0x8c, 0x3e, 0xe2, 0x13, 0xa8, 0x08,
+	0xc4, 0x16, 0xe9, 0xf6, 0x3a, 0xd8, 0x09, 0xa9, 0x98, 0x8a, 0x45, 0x2d, 0x73, 0xee, 0x15, 0x97,
+	0xd9, 0x47, 0xfe, 0x1c, 0x2e, 0xf1, 0x62, 0xea, 0x19, 0x76, 0x5a, 0x07, 0x43, 0x0a, 0x4f, 0xc4,
+	0x43, 0x33, 0xf6, 0x07, 0x94, 0x7b, 0x40, 0x69, 0x0d, 0xae, 0x06, 0xa1, 0xa3, 0x34, 0x4f, 0xc7,
+	0xc2, 0x5f, 0xf6, 0xe1, 0x23, 0xb4, 0xdf, 0x83, 0x39, 0xcf, 0xab, 0x86, 0x74, 0xcf, 0xc4, 0x82,
+	0x5f, 0xf4, 0x98, 0x07, 0x54, 0x7f, 0x0a, 0x57, 0x7c, 0xdc, 0x28, 0xc5, 0xb3, 0xb1, 0xd8, 0x97,
+	0x3c, 0x80, 0x08, 0xbd, 0x7f, 0x19, 0xae, 0x88, 0x4a, 0xa1, 0xdf, 0xeb, 0x10, 0x4d, 0x1f, 0xd2,
+	0x3d, 0x17, 0x8b, 0x5f, 0xe1, 0x00, 0xbb, 0x8c, 0x7f, 0x40, 0x7d, 0x0c, 0xd7, 0xc3, 0xf0, 0x51,
+	0x4b, 0x80, 0x58, 0x11, 0x6f, 0x07, 0x45, 0x44, 0xac, 0x62, 0x05, 0x90, 0xd6, 0x3f, 0x36, 0x3a,
+	0x86, 0x66, 0x9d, 0xa8, 0x5d, 0x71, 0xb8, 0xca, 0x79, 0x96, 0x51, 0x66, 0x86, 0x70, 0x97, 0xcc,
+	0x13, 0x65, 0xda, 0x9b, 0xef, 0x9e, 0x45, 0xf9, 0x6f, 0xd3, 0x50, 0xe0, 0xc7, 0x53, 0xc1, 0x76,
+	0xbf, 0xe3, 0xa0, 0xcd, 0x81, 0x72, 0x91, 0x67, 0xfa, 0x1b, 0xb1, 0x11, 0x65, 0xcb, 0xab, 0x27,
+	0xc3, 0xb5, 0xe5, 0x57, 0x30, 0x13, 0xc0, 0x53, 0xbd, 0x94, 0x0f, 0x23, 0x56, 0x10, 0x1c, 0x57,
+	0x84, 0x48, 0x05, 0xf9, 0xd0, 0x4d, 0x37, 0xfd, 0x3f, 0x86, 0xa2, 0x90, 0xe0, 0x81, 0x17, 0xce,
+	0x04, 0x3e, 0x45, 0x82, 0xaf, 0x36, 0x52, 0x23, 0xcb, 0x62, 0x5e, 0xab, 0xdc, 0x1a, 0x11, 0xdb,
+	0x4b, 0x21, 0x51, 0x85, 0xf4, 0xaf, 0xc0, 0xdc, 0x80, 0x80, 0x13, 0x7f, 0x0d, 0xf9, 0x33, 0xad,
+	0xe1, 0x62, 0x58, 0xca, 0x89, 0xb7, 0x98, 0x4b, 0x90, 0xc3, 0xc7, 0x86, 0xa3, 0xb6, 0x68, 0xda,
+	0xa7, 0x21, 0x6b, 0x42, 0xc9, 0x52, 0xc2, 0x0a, 0xcd, 0x3a, 0x34, 0x96, 0x3b, 0x3a, 0xa1, 0x97,
+	0x3a, 0xed, 0x88, 0x45, 0x9d, 0x82, 0x92, 0xe3, 0x14, 0x45, 0x3b, 0x42, 0xeb, 0x30, 0x29, 0x86,
+	0x45, 0xb2, 0x4b, 0x8f, 0x97, 0xec, 0x0a, 0x9c, 0x5b, 0x24, 0x13, 0x2e, 0x0c, 0x5b, 0x16, 0x13,
+	0x96, 0xf1, 0x84, 0x61, 0xcb, 0xf2, 0x85, 0xd1, 0x61, 0x21, 0x2c, 0x3b, 0xbe, 0x30, 0x6c, 0x59,
+	0x42, 0xd8, 0x33, 0x40, 0x7e, 0x28, 0xf1, 0x0e, 0x09, 0x3f, 0xdf, 0x1f, 0xc4, 0x42, 0x46, 0x27,
+	0x30, 0x65, 0xda, 0x9b, 0xe4, 0x92, 0x1e, 0xa5, 0xb2, 0x52, 0x29, 0x21, 0xff, 0x46, 0x02, 0xc0,
+	0x3f, 0x08, 0x34, 0x6d, 0xd2, 0x1b, 0x92, 0x9b, 0x36, 0xe9, 0xf3, 0xff, 0x52, 0x89, 0x54, 0x81,
+	0x6c, 0x8b, 0x98, 0x0e, 0xbb, 0x84, 0xf2, 0xed, 0xf4, 0xde, 0xa3, 0xca, 0x86, 0xcc, 0xf9, 0x96,
+	0x4f, 0xe9, 0x52, 0x46, 0xfe, 0x4e, 0x82, 0xd4, 0x8e, 0x85, 0x31, 0xfa, 0x18, 0x52, 0x16, 0x21,
+	0x6e, 0xd7, 0x61, 0x61, 0xf4, 0x2b, 0x83, 0xc2, 0xf8, 0xd0, 0x03, 0xc8, 0xb6, 0x0e, 0x8c, 0x8e,
+	0x6e, 0x61, 0x53, 0x04, 0xa3, 0x71, 0x30, 0x3c, 0x5e, 0xb9, 0x0f, 0xc5, 0x81, 0x23, 0x19, 0xb9,
+	0x3f, 0x0f, 0x21, 0xef, 0x58, 0x18, 0xbb, 0xce, 0x97, 0x1c, 0x6f, 0x93, 0x80, 0xf2, 0xf2, 0xe7,
+	0x47, 0xa9, 0x6c, 0xa2, 0x94, 0x94, 0xbf, 0x97, 0x60, 0x32, 0x74, 0x44, 0x23, 0xa5, 0xfe, 0x5f,
+	0x95, 0x78, 0x37, 0xa1, 0xd8, 0x70, 0x99, 0xb6, 0x49, 0xc7, 0x68, 0x9d, 0x50, 0xd7, 0xe9, 0x59,
+	0x06, 0xb1, 0x0c, 0xe7, 0x84, 0xa9, 0x38, 0xa1, 0x78, 0xef, 0xf2, 0x2d, 0x40, 0x3c, 0x4d, 0xd8,
+	0xac, 0xbf, 0x32, 0x02, 0xc7, 0x37, 0x49, 0x98, 0x12, 0xa7, 0x48, 0xc1, 0x2f, 0xfa, 0xae, 0x03,
+	0x9b, 0xb6, 0xa3, 0x99, 0x2d, 0xac, 0x06, 0x6a, 0xdd, 0x82, 0x4b, 0xdc, 0xa4, 0x35, 0xef, 0x02,
+	0x4c, 0xdb, 0xcf, 0x8d, 0x1e, 0xef, 0xec, 0xa8, 0x1d, 0x42, 0x9e, 0xf7, 0x79, 0xfd, 0x96, 0x55,
+	0x8a, 0x74, 0x80, 0xc9, 0x5f, 0x67, 0x64, 0x1a, 0x31, 0x34, 0x76, 0x40, 0xcf, 0x1a, 0x9e, 0x38,
+	0xb7, 0x88, 0x18, 0xbf, 0x04, 0x25, 0x3f, 0x62, 0xf4, 0xd8, 0x0a, 0xc5, 0xf9, 0xb8, 0x35, 0x62,
+	0xbc, 0xf0, 0x6c, 0xa9, 0x14, 0xf1, 0x80, 0x71, 0x31, 0xcc, 0x58, 0xdc, 0x80, 0x62, 0x65, 0x42,
+	0x00, 0x8f, 0x71, 0x77, 0x62, 0x05, 0x0c, 0x5b, 0x5f, 0x41, 0xd6, 0x10, 0x8d, 0xbb, 0x1e, 0xbf,
+	0xbd, 0x8a, 0xdb, 0xcc, 0x0b, 0xc8, 0xac, 0x93, 0x36, 0x8b, 0x4a, 0x7e, 0x04, 0x92, 0xce, 0x16,
+	0x81, 0xde, 0x85, 0xa9, 0x83, 0x7e, 0x57, 0x33, 0x55, 0x0b, 0x6b, 0x3a, 0x0b, 0x41, 0x09, 0xb6,
+	0x31, 0x93, 0x8c, 0xaa, 0x08, 0xa2, 0xfc, 0xdb, 0x49, 0xd7, 0xb9, 0xb0, 0x82, 0xed, 0x1e, 0x31,
+	0x6d, 0x8c, 0x1a, 0x90, 0xe6, 0xea, 0x0a, 0xd9, 0xf1, 0x37, 0xdf, 0x60, 0x71, 0xa2, 0x08, 0x66,
+	0xea, 0x42, 0xcc, 0x7c, 0xba, 0x2a, 0xd0, 0xb8, 0x02, 0x05, 0x4e, 0x14, 0x95, 0xcc, 0x02, 0xa4,
+	0x79, 0x4f, 0x54, 0x1c, 0x62, 0xe4, 0xd6, 0x44, 0x56, 0xaf, 0x55, 0x6d, 0xb2, 0x11, 0x45, 0xcc,
+	0x40, 0x1a, 0xe4, 0x6d, 0x6c, 0x1d, 0x62, 0x4b, 0xed, 0x90, 0x36, 0x6f, 0x7d, 0xe5, 0xeb, 0xbf,
+	0x30, 0x6a, 0x7e, 0x70, 0x97, 0x57, 0x6d, 0x32, 0x8c, 0x75, 0xd2, 0xb6, 0x1b, 0xa6, 0x63, 0x9d,
+	0x28, 0x60, 0x7b, 0x04, 0x54, 0x86, 0x4c, 0x17, 0xdb, 0xb6, 0xd6, 0xc6, 0x2c, 0x22, 0xe7, 0x14,
+	0xf7, 0xb5, 0xd2, 0x86, 0xe2, 0x00, 0x23, 0x2a, 0x41, 0xf2, 0x39, 0x3e, 0x11, 0x27, 0x83, 0x3e,
+	0xa2, 0x8f, 0x83, 0x0d, 0x86, 0x7c, 0x7d, 0x3e, 0x56, 0x37, 0xb1, 0xdd, 0xa2, 0x15, 0xb1, 0x98,
+	0xb8, 0x2f, 0xc9, 0x9a, 0x7b, 0x16, 0x79, 0xc9, 0xdd, 0xc6, 0xf2, 0x16, 0x4c, 0xb0, 0x4b, 0x3e,
+	0xca, 0x43, 0x66, 0x77, 0xf3, 0xb3, 0xcd, 0xad, 0xc7, 0x9b, 0xa5, 0x9f, 0xa0, 0x22, 0xe4, 0x57,
+	0x96, 0x56, 0x1e, 0x36, 0xd4, 0x95, 0x87, 0x8d, 0x95, 0xcf, 0x4a, 0x12, 0x02, 0x48, 0xff, 0xe2,
+	0x6e, 0x63, 0xb7, 0xb1, 0x5a, 0x4a, 0xa0, 0x49, 0xc8, 0x35, 0x9e, 0x34, 0x56, 0x76, 0x77, 0xd6,
+	0x36, 0x3f, 0x2d, 0x25, 0xe9, 0xeb, 0xca, 0xd6, 0xc6, 0xf6, 0x7a, 0x63, 0xa7, 0xb1, 0x5a, 0x4a,
+	0xc9, 0xbf, 0x93, 0x80, 0xb2, 0xb0, 0xca, 0x96, 0xdb, 0x29, 0xf7, 0x2e, 0x7e, 0x9f, 0xc1, 0x84,
+	0x4d, 0xc5, 0xb2, 0x75, 0x4d, 0xd5, 0xef, 0x8e, 0x7e, 0x9e, 0x98, 0xb6, 0x55, 0xde, 0x8f, 0xe0,
+	0x18, 0xc3, 0xa7, 0x3e, 0xf1, 0x26, 0xa7, 0xfe, 0x3d, 0x40, 0xa2, 0xc4, 0xb1, 0x1d, 0x0b, 0x6b,
+	0x5d, 0x1e, 0x99, 0x92, 0xbc, 0xd5, 0xc8, 0x47, 0x9a, 0x6c, 0x80, 0x45, 0x27, 0x3e, 0x9b, 0xd6,
+	0x28, 0xc1, 0xd9, 0x29, 0x6f, 0x36, 0xb6, 0x2c, 0x7f, 0xb6, 0xbc, 0x00, 0x33, 0x8f, 0x35, 0xc3,
+	0xf1, 0x16, 0xe3, 0x06, 0xc2, 0x88, 0xbb, 0xbe, 0xfc, 0x4d, 0x02, 0x66, 0x3f, 0xc5, 0x4e, 0xc8,
+	0xeb, 0xc7, 0x89, 0x9b, 0xe7, 0x6b, 0x15, 0x26, 0xb2, 0x63, 0x98, 0x58, 0xe5, 0x26, 0x10, 0x11,
+	0xb8, 0xc0, 0x89, 0x4d, 0x46, 0x0b, 0x4f, 0xc2, 0x96, 0xe5, 0x15, 0x24, 0xee, 0x24, 0x6c, 0x59,
+	0xa8, 0x0a, 0x6f, 0x89, 0x49, 0xa1, 0xdb, 0xc5, 0x04, 0xef, 0x31, 0xf3, 0x21, 0xbf, 0x72, 0xb2,
+	0xe5, 0xbf, 0x49, 0xc0, 0xdc, 0x6e, 0x4f, 0xd7, 0x1c, 0xfc, 0xff, 0xc4, 0x14, 0x8a, 0x87, 0x26,
+	0x42, 0x4e, 0xf2, 0x2c, 0x01, 0x4c, 0x60, 0x8a, 0x08, 0x75, 0x5a, 0x36, 0x48, 0x9d, 0x6b, 0x36,
+	0x90, 0x7f, 0x53, 0x82, 0x8b, 0x0f, 0x0c, 0x53, 0xdf, 0x30, 0x6c, 0xdb, 0x30, 0xdb, 0xcb, 0x1d,
+	0xb2, 0x6f, 0x8f, 0x65, 0xc9, 0x47, 0x50, 0xd8, 0xef, 0x90, 0x7d, 0x61, 0x47, 0xf7, 0x4e, 0x38,
+	0xb2, 0x21, 0xf3, 0x94, 0x99, 0x3f, 0xdb, 0x72, 0x1f, 0xca, 0xc3, 0xba, 0x88, 0xec, 0xf0, 0x39,
+	0xcc, 0x74, 0x39, 0x5d, 0x7d, 0x13, 0x79, 0xa8, 0xeb, 0x83, 0xbb, 0x62, 0xff, 0x5b, 0x82, 0x8b,
+	0xcb, 0x9a, 0xd3, 0x3a, 0xe0, 0x4e, 0x35, 0xbe, 0x0d, 0xbe, 0x84, 0xac, 0xc5, 0xe7, 0xbb, 0xfa,
+	0xc4, 0xa7, 0x87, 0x53, 0x04, 0x56, 0xc5, 0x5f, 0xc5, 0x43, 0xac, 0x3c, 0x85, 0x8c, 0xab, 0xcd,
+	0x1b, 0xa7, 0x67, 0x04, 0x29, 0x76, 0xc9, 0x49, 0xf0, 0x8f, 0x5e, 0xec, 0x9a, 0xff, 0x9f, 0x12,
+	0x94, 0x87, 0xb5, 0x11, 0x66, 0xff, 0x0a, 0x72, 0x96, 0x78, 0x76, 0x3b, 0xd2, 0xcb, 0x67, 0x58,
+	0x9b, 0xc8, 0x81, 0xee, 0x83, 0xe2, 0x83, 0x56, 0x8e, 0x20, 0xeb, 0x49, 0x7b, 0xe3, 0xf5, 0xf9,
+	0x79, 0x3d, 0x11, 0x97, 0xd7, 0xe5, 0x5f, 0x85, 0x0b, 0x4c, 0x51, 0x5a, 0x94, 0x8c, 0xbf, 0xe7,
+	0x4b, 0x90, 0x39, 0xa3, 0x0b, 0xba, 0x7c, 0xf2, 0xaf, 0x27, 0x60, 0x76, 0x50, 0x03, 0x61, 0x88,
+	0xa7, 0xc3, 0x66, 0x1f, 0xd1, 0xa5, 0x86, 0xb0, 0x22, 0x8d, 0xfe, 0x5b, 0xd2, 0x79, 0x5a, 0x3d,
+	0xc2, 0xab, 0xc6, 0xa9, 0xb0, 0xe4, 0xbf, 0x90, 0x60, 0xea, 0x53, 0xec, 0xd0, 0x2b, 0xe1, 0x58,
+	0x7b, 0xf0, 0x10, 0xf2, 0x6f, 0xf0, 0xa1, 0x19, 0x2c, 0xff, 0x1b, 0xf3, 0x25, 0xc8, 0xf5, 0xb4,
+	0x36, 0x56, 0x6d, 0xe3, 0x25, 0xcf, 0xec, 0xf4, 0x9e, 0xa2, 0xb5, 0x71, 0xd3, 0x78, 0xc9, 0x3a,
+	0x20, 0x6c, 0xd0, 0x21, 0xcf, 0xb1, 0x29, 0x32, 0x39, 0x9b, 0xbe, 0x43, 0x09, 0xf2, 0xb7, 0x12,
+	0x14, 0x3d, 0xed, 0x85, 0x49, 0xd7, 0xc3, 0x9f, 0xc4, 0xa4, 0xb1, 0xef, 0xa6, 0xa1, 0xcf, 0x61,
+	0x3f, 0x85, 0xa2, 0x89, 0x8f, 0x1d, 0x35, 0xa0, 0x05, 0xbf, 0x0a, 0x4e, 0x52, 0xf2, 0xb6, 0xa7,
+	0xc9, 0x47, 0xac, 0x3e, 0x58, 0xd1, 0x7a, 0xda, 0xbe, 0xd1, 0x31, 0xd8, 0xd5, 0x6e, 0x0c, 0x73,
+	0xca, 0x7f, 0x9d, 0x04, 0xc4, 0x8b, 0xcd, 0x20, 0x04, 0xd2, 0x00, 0xf1, 0x0c, 0xd4, 0x0a, 0x50,
+	0x85, 0xab, 0xc4, 0x7f, 0xbf, 0x64, 0xc9, 0x26, 0xa4, 0xd2, 0x74, 0x6b, 0x90, 0x84, 0xba, 0x30,
+	0x1b, 0x68, 0xea, 0x06, 0xc5, 0xf0, 0x3d, 0xbd, 0x37, 0x7a, 0x35, 0x18, 0x12, 0x75, 0x01, 0x47,
+	0x91, 0xd1, 0x36, 0xcc, 0xea, 0xb8, 0x67, 0xe1, 0x96, 0xe6, 0x60, 0x5d, 0xd5, 0x7a, 0x86, 0x7a,
+	0x88, 0x2d, 0xdb, 0x20, 0xa6, 0xf7, 0x15, 0x20, 0x28, 0x4e, 0xfc, 0x28, 0xa4, 0x89, 0xbb, 0x7b,
+	0xd8, 0x52, 0x66, 0x7c, 0xce, 0xa5, 0x9e, 0xb1, 0xc7, 0xf9, 0xd0, 0x32, 0x14, 0x3b, 0xe4, 0x28,
+	0x04, 0x95, 0x8a, 0x85, 0x9a, 0xec, 0x90, 0xa3, 0x00, 0xc6, 0x2a, 0x94, 0x0e, 0x8c, 0xf6, 0x41,
+	0x08, 0x64, 0x22, 0x16, 0x64, 0x8a, 0xf2, 0xf8, 0x28, 0xb2, 0x0d, 0x53, 0xdc, 0xa7, 0x1f, 0xf4,
+	0x4d, 0x56, 0x4f, 0xc8, 0x5a, 0x64, 0x1d, 0x0f, 0x90, 0x6e, 0x3e, 0x5c, 0xaa, 0xdf, 0xbd, 0x57,
+	0x92, 0x50, 0x16, 0x52, 0xcd, 0x87, 0x4b, 0xb7, 0x4b, 0x09, 0x94, 0x81, 0xe4, 0xc6, 0xea, 0xdd,
+	0x52, 0x92, 0x3e, 0xec, 0x35, 0xb7, 0x4a, 0x29, 0x31, 0xef, 0xce, 0xfd, 0xf7, 0x4b, 0x13, 0xe2,
+	0xf9, 0xee, 0xed, 0x7a, 0x29, 0x4d, 0xc1, 0x36, 0x76, 0x95, 0x8d, 0x5d, 0xe5, 0x4e, 0x29, 0x23,
+	0x3f, 0x80, 0x2b, 0xbc, 0x94, 0x61, 0xbb, 0xcd, 0x23, 0x7f, 0xc8, 0xe2, 0xef, 0xc2, 0x54, 0x9f,
+	0x51, 0x55, 0x6c, 0xd2, 0x0b, 0xa0, 0xce, 0xfc, 0x27, 0xab, 0x4c, 0x72, 0x6a, 0x83, 0x13, 0xe5,
+	0xbf, 0x97, 0x60, 0x66, 0x5b, 0xb4, 0x07, 0x42, 0xfc, 0x2d, 0x00, 0xd1, 0x36, 0xf0, 0x8f, 0xd3,
+	0x4a, 0xfc, 0xb7, 0xf3, 0x08, 0x28, 0x8f, 0xa8, 0x68, 0x66, 0x1b, 0x2b, 0x01, 0xd8, 0xca, 0x2e,
+	0x4c, 0x86, 0x06, 0xd1, 0x75, 0x28, 0x74, 0x0d, 0x53, 0x1d, 0x68, 0x60, 0xe4, 0xbb, 0x86, 0xe9,
+	0xce, 0x63, 0x53, 0xb4, 0x63, 0x7f, 0x4a, 0x42, 0x4c, 0xd1, 0x8e, 0xdd, 0x29, 0xf2, 0x36, 0x5c,
+	0x12, 0xed, 0x9d, 0xa5, 0x7d, 0x9b, 0x74, 0xfa, 0x0e, 0xde, 0xd6, 0x9c, 0x83, 0xa6, 0x63, 0x69,
+	0x0e, 0x6e, 0x9f, 0xc8, 0xb7, 0x23, 0xb7, 0x67, 0x0a, 0x60, 0x75, 0xad, 0xb9, 0xb4, 0xbe, 0xbe,
+	0xf5, 0xb8, 0xb1, 0x5a, 0x92, 0xe8, 0xa0, 0xfb, 0x92, 0x90, 0x6f, 0x02, 0xac, 0x90, 0x6e, 0xcf,
+	0xc2, 0xb6, 0x4d, 0x2c, 0xf9, 0xaa, 0x0b, 0x50, 0x80, 0xec, 0xda, 0x6a, 0x63, 0x73, 0x67, 0x6d,
+	0xe7, 0xf3, 0xd2, 0x4f, 0xe8, 0xa6, 0x7e, 0xd1, 0xdc, 0x59, 0x2d, 0x49, 0xf2, 0x1f, 0x4e, 0xc0,
+	0xf4, 0xd0, 0x31, 0x44, 0x4f, 0xa1, 0xc8, 0xe3, 0xa6, 0xfa, 0x4c, 0x78, 0x0a, 0xb3, 0xeb, 0x28,
+	0x57, 0xaf, 0xb0, 0x83, 0x89, 0xab, 0xd7, 0x94, 0x1e, 0xa2, 0xa2, 0x6f, 0x25, 0xb8, 0x26, 0xaa,
+	0x62, 0x1e, 0x3e, 0x84, 0x03, 0x44, 0x1c, 0xef, 0x8f, 0x47, 0x2c, 0x94, 0x4f, 0xf1, 0x2e, 0xe5,
+	0x8a, 0xf6, 0x5a, 0xe7, 0xeb, 0xc3, 0x25, 0x51, 0x42, 0x8b, 0x2d, 0x09, 0xeb, 0xc0, 0xcf, 0xfc,
+	0xdd, 0x33, 0x79, 0x93, 0x32, 0xc7, 0x90, 0x23, 0x7d, 0x76, 0x11, 0x2a, 0xd4, 0x35, 0xf6, 0x69,
+	0x56, 0x56, 0x1d, 0xe2, 0x68, 0x1d, 0x35, 0xf0, 0x51, 0x34, 0xc5, 0x3e, 0x8a, 0xce, 0x76, 0xb5,
+	0x63, 0x96, 0xb6, 0x77, 0xe8, 0x78, 0xd3, 0xfd, 0x42, 0x4a, 0x8d, 0xf7, 0xb6, 0xe8, 0xf6, 0xab,
+	0x9a, 0xf0, 0x1a, 0xf6, 0x8b, 0x1a, 0x7a, 0xa9, 0x64, 0x7e, 0xc3, 0x42, 0xc3, 0xd4, 0x08, 0xc5,
+	0xd8, 0x6b, 0x7c, 0x4f, 0xec, 0xdc, 0x25, 0xfb, 0xf4, 0x29, 0x48, 0x87, 0x19, 0xbb, 0xdf, 0xeb,
+	0x11, 0x8b, 0x86, 0xca, 0x96, 0xe7, 0x75, 0xe5, 0x34, 0xf3, 0x95, 0xdb, 0xa3, 0xfc, 0x28, 0x49,
+	0xb0, 0x08, 0x69, 0x6f, 0x79, 0x70, 0x01, 0x1f, 0xfe, 0xc7, 0x04, 0x5c, 0x88, 0x0c, 0xe1, 0xd1,
+	0x6e, 0x2a, 0x9d, 0x9f, 0x9b, 0x5e, 0x87, 0x02, 0x65, 0xf2, 0xe2, 0x12, 0xef, 0x16, 0xe5, 0x29,
+	0x4d, 0x44, 0x25, 0xf4, 0x0a, 0xae, 0x06, 0xba, 0x7e, 0xe7, 0xef, 0x43, 0xfe, 0x07, 0xcd, 0x53,
+	0xfc, 0x68, 0xce, 0xdf, 0x81, 0xe1, 0x8e, 0x2f, 0xbd, 0x24, 0x5f, 0xf4, 0x26, 0x84, 0x3b, 0xbb,
+	0xf2, 0x06, 0xe4, 0x77, 0x08, 0xe9, 0xac, 0x62, 0x47, 0x33, 0x3a, 0xec, 0x43, 0x8f, 0x43, 0x48,
+	0x27, 0x58, 0x02, 0x64, 0x29, 0x81, 0x55, 0x53, 0xd7, 0xa1, 0xc0, 0x06, 0xdd, 0xdc, 0xc3, 0x4b,
+	0x8c, 0x3c, 0xa5, 0xb9, 0xc9, 0xe5, 0x5f, 0x25, 0x28, 0x8a, 0x92, 0xc2, 0x6b, 0xdc, 0x6c, 0x09,
+	0x36, 0x9d, 0xcb, 0x10, 0x85, 0x41, 0xfc, 0xef, 0x75, 0x02, 0x7a, 0x71, 0x21, 0x01, 0x25, 0x45,
+	0xdc, 0x30, 0x74, 0xa1, 0x44, 0x96, 0x13, 0xd6, 0x74, 0xf4, 0x1e, 0x20, 0x26, 0xcd, 0x30, 0x0f,
+	0x49, 0x4b, 0x73, 0x67, 0x89, 0x5e, 0x0c, 0x1d, 0x59, 0xf3, 0x06, 0xd6, 0x74, 0x6a, 0xba, 0x16,
+	0xb1, 0x2c, 0xdc, 0x61, 0x89, 0xde, 0xe7, 0xb1, 0x29, 0x13, 0x2f, 0xe4, 0x2e, 0xfa, 0x13, 0x7c,
+	0x56, 0x7b, 0x4d, 0xaf, 0xff, 0x79, 0x02, 0x72, 0x9e, 0x4b, 0xa2, 0xef, 0x24, 0xc8, 0x88, 0xde,
+	0x15, 0xaa, 0x8d, 0xde, 0xfb, 0x63, 0xa6, 0xaa, 0x5c, 0x71, 0x6b, 0xdf, 0xc0, 0x8f, 0x45, 0xab,
+	0x5e, 0x0b, 0x4c, 0xbe, 0xfd, 0x6b, 0xff, 0xf0, 0x2f, 0xbf, 0x9b, 0xb8, 0x21, 0xff, 0xb4, 0x76,
+	0x58, 0xaf, 0x7d, 0x1d, 0xaa, 0xd3, 0x3e, 0x5a, 0x58, 0x78, 0x55, 0xe3, 0x8b, 0xb7, 0x17, 0xb9,
+	0x08, 0xbc, 0x28, 0x2d, 0xdc, 0x92, 0xd0, 0x1f, 0x48, 0x30, 0x19, 0xea, 0x1c, 0xa1, 0x78, 0xef,
+	0x8b, 0xea, 0x34, 0x8d, 0xa7, 0x1c, 0xd3, 0xc9, 0xff, 0x99, 0x6b, 0x6d, 0x61, 0xe1, 0xd5, 0xe2,
+	0x51, 0x10, 0x95, 0x29, 0x57, 0xff, 0xa7, 0x24, 0xe4, 0x03, 0x21, 0x1b, 0xfd, 0x33, 0x2f, 0x91,
+	0x43, 0x1f, 0x93, 0xe3, 0xbf, 0xb0, 0x45, 0xf7, 0xba, 0x2a, 0xe3, 0xb5, 0x55, 0xe4, 0x2f, 0xd9,
+	0x02, 0xf6, 0xd0, 0xce, 0x6b, 0xad, 0x2b, 0xda, 0x25, 0xb5, 0xaf, 0x43, 0x6d, 0xa1, 0xea, 0x81,
+	0x66, 0x1f, 0xbc, 0x1a, 0x24, 0xfa, 0x51, 0xfc, 0x15, 0xfa, 0x0f, 0x09, 0xd0, 0x70, 0x2f, 0x0a,
+	0x2d, 0xc6, 0xea, 0x78, 0x6a, 0x03, 0x6b, 0xdc, 0xf5, 0x3d, 0x67, 0xeb, 0xc3, 0x95, 0x1f, 0x65,
+	0x7d, 0x8b, 0xe1, 0xc6, 0x56, 0xfd, 0xfb, 0x34, 0xcc, 0xad, 0xf0, 0x8f, 0x85, 0x4b, 0xba, 0x4e,
+	0x83, 0x37, 0x0d, 0x91, 0x4d, 0x87, 0x58, 0x5a, 0x1b, 0xa3, 0xbf, 0x94, 0xa0, 0x34, 0xd8, 0xc0,
+	0x41, 0xf7, 0x47, 0xf8, 0x3d, 0x61, 0x64, 0xff, 0xa9, 0xf2, 0xe1, 0x19, 0x38, 0xf9, 0xfd, 0x4b,
+	0xbe, 0xc3, 0x8c, 0x72, 0x53, 0x9e, 0x3f, 0xc5, 0x28, 0xfb, 0x74, 0xf6, 0xe2, 0x33, 0x9f, 0x7d,
+	0x51, 0x5a, 0x60, 0xea, 0x0f, 0xb6, 0x2e, 0x46, 0x50, 0xff, 0x94, 0x4e, 0xce, 0x08, 0xea, 0x9f,
+	0xd6, 0x27, 0x19, 0x51, 0xfd, 0x7d, 0x9f, 0x9d, 0xaa, 0xff, 0x67, 0x12, 0x4c, 0x85, 0x5b, 0x00,
+	0xe8, 0xde, 0xd8, 0x3d, 0x03, 0xae, 0xfa, 0x07, 0x67, 0xec, 0x35, 0xc4, 0x86, 0xb2, 0x80, 0xe2,
+	0x94, 0x99, 0xaa, 0xfd, 0x83, 0x04, 0x19, 0x71, 0x7d, 0x1e, 0x21, 0xb2, 0x86, 0xdb, 0x04, 0x95,
+	0x5b, 0xa3, 0x33, 0x08, 0x0d, 0x9f, 0x30, 0x0d, 0x15, 0xb4, 0xfd, 0x3a, 0x0d, 0x6b, 0x5f, 0x07,
+	0xfa, 0x0a, 0xee, 0x21, 0x09, 0x92, 0x82, 0x47, 0xa4, 0xcd, 0x25, 0xdc, 0x92, 0xea, 0x7f, 0x25,
+	0x41, 0x21, 0x94, 0xba, 0xff, 0x94, 0xc7, 0xbd, 0x10, 0x6d, 0xa4, 0xb8, 0x17, 0x71, 0x87, 0xaf,
+	0xc4, 0xf7, 0x7c, 0x87, 0x2f, 0xef, 0xf2, 0x0d, 0xb6, 0xdc, 0x77, 0xd1, 0x3b, 0xa7, 0x2c, 0x37,
+	0x58, 0xc5, 0x2c, 0x5b, 0x10, 0xf7, 0x5f, 0x13, 0xcb, 0x33, 0x0a, 0x23, 0xfa, 0x1f, 0x33, 0x2d,
+	0xe2, 0x90, 0x6d, 0xe9, 0x8b, 0x22, 0x9f, 0xec, 0xcd, 0xfd, 0xa3, 0x44, 0x52, 0x69, 0x3c, 0xf9,
+	0x93, 0xc4, 0xd5, 0x65, 0x06, 0xb8, 0xcc, 0x00, 0x39, 0xaf, 0x7f, 0x57, 0xaf, 0xee, 0xd5, 0xf7,
+	0xd3, 0xec, 0x57, 0x48, 0x77, 0xfe, 0x27, 0x00, 0x00, 0xff, 0xff, 0x89, 0x45, 0xb0, 0xed, 0xe8,
+	0x31, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -4077,6 +4171,11 @@ type ExecutionClient interface {
 	// where, for each requested blob not present in the CAS, there is a
 	// `Violation` with a `type` of `MISSING` and a `subject` of
 	// `"blobs/{hash}/{size}"` indicating the digest of the missing blob.
+	//
+	// The server does not need to guarantee that a call to this method leads to
+	// at most one execution of the action. The server MAY execute the action
+	// multiple times, potentially in parallel. These redundant executions MAY
+	// continue to run, even if the operation is completed.
 	Execute(ctx context.Context, in *ExecuteRequest, opts ...grpc.CallOption) (Execution_ExecuteClient, error)
 	// Wait for an execution operation to complete. When the client initially
 	// makes the request, the server immediately responds with the current status
@@ -4224,6 +4323,11 @@ type ExecutionServer interface {
 	// where, for each requested blob not present in the CAS, there is a
 	// `Violation` with a `type` of `MISSING` and a `subject` of
 	// `"blobs/{hash}/{size}"` indicating the digest of the missing blob.
+	//
+	// The server does not need to guarantee that a call to this method leads to
+	// at most one execution of the action. The server MAY execute the action
+	// multiple times, potentially in parallel. These redundant executions MAY
+	// continue to run, even if the operation is completed.
 	Execute(*ExecuteRequest, Execution_ExecuteServer) error
 	// Wait for an execution operation to complete. When the client initially
 	// makes the request, the server immediately responds with the current status
diff --git a/go.mod b/go.mod
index 3d1a21f..f24a6c4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,16 +3,17 @@ module github.com/buchgr/bazel-remote
 require (
 	cloud.google.com/go v0.61.0 // indirect
 	github.com/abbot/go-http-auth v0.4.1-0.20181019201920-860ed7f246ff
-	github.com/bazelbuild/remote-apis v0.0.0-20200708200203-1252343900d9
 	github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
 	github.com/djherbis/atime v1.0.0
 	github.com/golang/protobuf v1.4.2
 	github.com/google/go-cmp v0.5.0
 	github.com/google/uuid v1.1.1
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
+	github.com/klauspost/compress v1.11.4
 	github.com/klauspost/cpuid v1.3.1 // indirect
 	github.com/minio/minio-go/v7 v7.0.1
 	github.com/mostynb/go-grpc-compression v1.1.4
+	github.com/mostynb/zstdpool-syncpool v0.0.2
 	github.com/prometheus/client_golang v1.7.1
 	github.com/slok/go-http-metrics v0.8.0
 	github.com/smartystreets/goconvey v1.6.4 // indirect
@@ -25,4 +26,4 @@ require (
 	gopkg.in/yaml.v2 v2.3.0
 )
 
-go 1.14
+go 1.15
diff --git a/go.sum b/go.sum
index 8c19f8d..2ec89ed 100644
--- a/go.sum
+++ b/go.sum
@@ -155,6 +155,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.11.3 h1:dB4Bn0tN3wdCzQxnS8r06kV74qN/TAfaIS0bVE8h3jc=
 github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
+github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
 github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
@@ -193,6 +195,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mostynb/go-grpc-compression v1.1.4 h1:wlscKqxoQsZQxUi6uTER+Gh9MbcUFUa4Cw5A69pj/c8=
 github.com/mostynb/go-grpc-compression v1.1.4/go.mod h1:rUYnd4NcUyZqHgjjISqjBMa6qTxMFjFu5+fHLI3k3Ho=
+github.com/mostynb/zstdpool-syncpool v0.0.2 h1:tSmtZSiSAfffEgy9ziKvOZOOlccUwF2Ar78+CeOH+8A=
+github.com/mostynb/zstdpool-syncpool v0.0.2/go.mod h1:j/I43b31x+jxmpN8j/o+jc9SdLDoTiDei95IvdCLWgA=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/main.go b/main.go
index a97b954..05d7d22 100644
--- a/main.go
+++ b/main.go
@@ -83,6 +83,12 @@ func main() {
 			Usage:   "The maximum size of the remote cache in GiB. This flag is required.",
 			EnvVars: []string{"BAZEL_REMOTE_MAX_SIZE"},
 		},
+		&cli.StringFlag{
+			Name:    "storage_mode",
+			Value:   "zstd",
+			Usage:   "Which format to store CAS blobs in. Must be one of \"zstd\" or \"uncompressed\".",
+			EnvVars: []string{"BAZEL_REMOTE_STORAGE_MODE"},
+		},
 		&cli.StringFlag{
 			Name:    "host",
 			Value:   "",
@@ -176,6 +182,30 @@ func main() {
 			Usage:   "When using proxy backends, sets the number of Goroutines to process parallel uploads to backend.",
 			EnvVars: []string{"BAZEL_REMOTE_NUM_UPLOADERS"},
 		},
+		&cli.StringFlag{
+			Name:    "http_proxy.url",
+			Value:   "",
+			Usage:   "The base URL to use for a http proxy backend.",
+			EnvVars: []string{"BAZEL_REMOTE_HTTP_PROXY_URL"},
+		},
+		&cli.StringFlag{
+			Name:    "gcs_proxy.bucket",
+			Value:   "",
+			Usage:   "The bucket to use for the Google Cloud Storage proxy backend.",
+			EnvVars: []string{"BAZEL_REMOTE_GCS_BUCKET"},
+		},
+		&cli.BoolFlag{
+			Name:    "gcs_proxy.use_default_credentials",
+			Value:   false,
+			Usage:   "Whether or not to use authentication for the Google Cloud Storage proxy backend.",
+			EnvVars: []string{"BAZEL_REMOTE_GCS_USE_DEFAULT_CREDENTIALS"},
+		},
+		&cli.StringFlag{
+			Name:    "gcs_proxy.json_credentials_file",
+			Value:   "",
+			Usage:   "Path to a JSON file that contains Google credentials for the Google Cloud Storage proxy backend.",
+			EnvVars: []string{"BAZEL_REMOTE_GCS_JSON_CREDENTIALS_FILE"},
+		},
 		&cli.StringFlag{
 			Name:    "s3.endpoint",
 			Value:   "",
@@ -284,9 +314,27 @@ func main() {
 					KeyVersion:      ctx.Int("s3.key_version"),
 				}
 			}
+
+			var hc *config.HTTPBackendConfig
+			if ctx.String("http_proxy.url") != "" {
+				hc = &config.HTTPBackendConfig{
+					BaseURL: ctx.String("http_proxy.url"),
+				}
+			}
+
+			var gcs *config.GoogleCloudStorageConfig
+			if ctx.String("gcs_proxy.bucket") != "" {
+				gcs = &config.GoogleCloudStorageConfig{
+					Bucket:                ctx.String("gcs_proxy.bucket"),
+					UseDefaultCredentials: ctx.Bool("gcs_proxy.use_default_credentials"),
+					JSONCredentialsFile:   ctx.String("gcs_proxy.json_credentials_file"),
+				}
+			}
+
 			c, err = config.New(
 				ctx.String("dir"),
 				ctx.Int("max_size"),
+				ctx.String("storage_mode"),
 				ctx.String("host"),
 				ctx.Int("port"),
 				ctx.Int("grpc_port"),
@@ -299,6 +347,8 @@ func main() {
 				ctx.String("tls_cert_file"),
 				ctx.String("tls_key_file"),
 				ctx.Duration("idle_timeout"),
+				hc,
+				gcs,
 				s3,
 				ctx.Bool("disable_http_ac_validation"),
 				ctx.Bool("disable_grpc_ac_deps_check"),
@@ -337,7 +387,7 @@ func main() {
 		if c.GoogleCloudStorage != nil {
 			proxyCache, err = gcsproxy.New(c.GoogleCloudStorage.Bucket,
 				c.GoogleCloudStorage.UseDefaultCredentials, c.GoogleCloudStorage.JSONCredentialsFile,
-				accessLogger, errorLogger, c.NumUploaders, c.MaxQueuedUploads)
+				c.StorageMode, accessLogger, errorLogger, c.NumUploaders, c.MaxQueuedUploads)
 			if err != nil {
 				log.Fatal(err)
 			}
@@ -348,13 +398,16 @@ func main() {
 			if err != nil {
 				log.Fatal(err)
 			}
-			proxyCache = httpproxy.New(baseURL,
+			proxyCache, err = httpproxy.New(baseURL, c.StorageMode,
 				httpClient, accessLogger, errorLogger, c.NumUploaders, c.MaxQueuedUploads)
+			if err != nil {
+				log.Fatal(err)
+			}
 		} else if c.S3CloudStorage != nil {
-			proxyCache = s3proxy.New(c.S3CloudStorage, accessLogger, errorLogger, c.NumUploaders, c.MaxQueuedUploads)
+			proxyCache = s3proxy.New(c.S3CloudStorage, c.StorageMode, accessLogger, errorLogger, c.NumUploaders, c.MaxQueuedUploads)
 		}
 
-		diskCache, err := disk.New(c.Dir, int64(c.MaxSize)*1024*1024*1024, proxyCache)
+		diskCache, err := disk.New(c.Dir, int64(c.MaxSize)*1024*1024*1024, c.StorageMode, proxyCache)
 		if err != nil {
 			log.Fatal(err)
 		}
diff --git a/server/BUILD.bazel b/server/BUILD.bazel
index f60cf80..b27555f 100644
--- a/server/BUILD.bazel
+++ b/server/BUILD.bazel
@@ -17,15 +17,19 @@ go_library(
     deps = [
         "//cache:go_default_library",
         "//cache/disk:go_default_library",
+        "//cache/disk/casblob:go_default_library",
         "//genproto/build/bazel/remote/asset/v1:go_default_library",
         "//genproto/build/bazel/remote/execution/v2:go_default_library",
         "//genproto/build/bazel/semver:go_default_library",
         "//utils/idle:go_default_library",
+        "//utils/zstdpool:go_default_library",
         "@com_github_abbot_go_http_auth//:go_default_library",
         "@com_github_golang_protobuf//jsonpb:go_default_library_gen",
         "@com_github_golang_protobuf//proto:go_default_library",
+        "@com_github_klauspost_compress//zstd:go_default_library",
         "@com_github_mostynb_go_grpc_compression//snappy:go_default_library",
         "@com_github_mostynb_go_grpc_compression//zstd:go_default_library",
+        "@com_github_mostynb_zstdpool_syncpool//:go_default_library",
         "@go_googleapis//google/bytestream:bytestream_go_proto",
         "@go_googleapis//google/rpc:code_go_proto",
         "@go_googleapis//google/rpc:status_go_proto",
@@ -49,11 +53,13 @@ go_test(
     deps = [
         "//cache:go_default_library",
         "//cache/disk:go_default_library",
+        "//cache/disk/casblob:go_default_library",
         "//genproto/build/bazel/remote/asset/v1:go_default_library",
         "//genproto/build/bazel/remote/execution/v2:go_default_library",
         "//utils:go_default_library",
         "@com_github_golang_protobuf//proto:go_default_library",
         "@com_github_google_uuid//:go_default_library",
+        "@com_github_klauspost_compress//zstd:go_default_library",
         "@go_googleapis//google/bytestream:bytestream_go_proto",
         "@org_golang_google_grpc//:go_default_library",
         "@org_golang_google_grpc//codes:go_default_library",
diff --git a/server/grpc.go b/server/grpc.go
index 6b988e8..035f7bb 100644
--- a/server/grpc.go
+++ b/server/grpc.go
@@ -103,6 +103,7 @@ func (s *grpcServer) GetCapabilities(ctx context.Context,
 			},
 			MaxBatchTotalSizeBytes:      0, // "no limit"
 			SymlinkAbsolutePathStrategy: pb.SymlinkAbsolutePathStrategy_ALLOWED,
+			SupportedCompressor:         []pb.Compressor_Value{pb.Compressor_ZSTD},
 		},
 		LowApiVersion:  &semver.SemVer{Major: int32(2)},
 		HighApiVersion: &semver.SemVer{Major: int32(2), Minor: int32(1)},
diff --git a/server/grpc_ac.go b/server/grpc_ac.go
index 130c644..6917172 100644
--- a/server/grpc_ac.go
+++ b/server/grpc_ac.go
@@ -56,7 +56,7 @@ func (s *grpcServer) GetActionResult(ctx context.Context,
 	if !s.depsCheck {
 		logPrefix = "GRPC AC GET NODEPSCHECK"
 
-		rdr, sizeBytes, err := s.cache.Get(cache.AC, req.ActionDigest.Hash, unknownActionResultSize)
+		rdr, sizeBytes, err := s.cache.Get(cache.AC, req.ActionDigest.Hash, unknownActionResultSize, 0)
 		if err != nil {
 			s.accessLogger.Printf("%s %s %s", logPrefix, req.ActionDigest.Hash, err)
 			return nil, status.Error(codes.Unknown, err.Error())
diff --git a/server/grpc_asset.go b/server/grpc_asset.go
index 366eeb0..559d127 100644
--- a/server/grpc_asset.go
+++ b/server/grpc_asset.go
@@ -70,7 +70,7 @@ func (s *grpcServer) FetchBlob(ctx context.Context, req *asset.FetchBlobRequest)
 
 			if size < 0 {
 				// We don't know the size yet (bad http backend?).
-				r, size, err := s.cache.Get(cache.CAS, sha256Str, -1)
+				r, size, err := s.cache.Get(cache.CAS, sha256Str, -1, 0)
 				if r != nil {
 					defer r.Close()
 				}
diff --git a/server/grpc_cas.go b/server/grpc_cas.go
index b227b82..8054ce9 100644
--- a/server/grpc_cas.go
+++ b/server/grpc_cas.go
@@ -100,7 +100,7 @@ func (s *grpcServer) getBlobData(hash string, size int64) ([]byte, error) {
 		return []byte{}, nil
 	}
 
-	rdr, sizeBytes, err := s.cache.Get(cache.CAS, hash, size)
+	rdr, sizeBytes, err := s.cache.Get(cache.CAS, hash, size, 0)
 	if err != nil {
 		rdr.Close()
 		return []byte{}, err
diff --git a/server/http.go b/server/http.go
index 7176ca6..fdf248a 100644
--- a/server/http.go
+++ b/server/http.go
@@ -20,10 +20,17 @@ import (
 	pb "github.com/buchgr/bazel-remote/genproto/build/bazel/remote/execution/v2"
 	"github.com/golang/protobuf/jsonpb"
 	"github.com/golang/protobuf/proto"
+	"github.com/klauspost/compress/zstd"
+
+	syncpool "github.com/mostynb/zstdpool-syncpool"
 )
 
 var blobNameSHA256 = regexp.MustCompile("^/?(.*/)?(ac/|cas/)([a-f0-9]{64})$")
 
+var decoder, _ = zstd.NewReader(nil) // TODO: raise WithDecoderConcurrency ?
+
+var singleDecoder = zstd.WithDecoderConcurrency(1)
+
 // HTTPCache ...
 type HTTPCache interface {
 	CacheHandler(w http.ResponseWriter, r *http.Request)
@@ -40,13 +47,14 @@ type httpCache struct {
 }
 
 type statusPageData struct {
-	CurrSize      int64
-	ReservedSize  int64
-	MaxSize       int64
-	NumFiles      int
-	ServerTime    int64
-	GitCommit     string
-	NumGoroutines int
+	CurrSize         int64
+	UncompressedSize int64
+	ReservedSize     int64
+	MaxSize          int64
+	NumFiles         int
+	ServerTime       int64
+	GitCommit        string
+	NumGoroutines    int
 }
 
 // NewHTTPCache returns a new instance of the cache.
@@ -55,7 +63,7 @@ type statusPageData struct {
 // be reported.
 func NewHTTPCache(cache *disk.Cache, accessLogger cache.Logger, errorLogger cache.Logger, validateAC bool, mangleACKeys bool, commit string) HTTPCache {
 
-	_, _, numItems := cache.Stats()
+	_, _, numItems, _ := cache.Stats()
 
 	errorLogger.Printf("Loaded %d existing disk cache items.", numItems)
 
@@ -205,7 +213,16 @@ func (h *httpCache) CacheHandler(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		rdr, sizeBytes, err := h.cache.Get(kind, hash, -1)
+		var rdr io.ReadCloser
+		var sizeBytes int64
+
+		zstdCompressed := false
+		if kind == cache.CAS && strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
+			rdr, sizeBytes, err = h.cache.GetZstd(hash, -1, 0)
+			zstdCompressed = true
+		} else {
+			rdr, sizeBytes, err = h.cache.Get(kind, hash, -1, 0)
+		}
 		if err != nil {
 			if e, ok := err.(*cache.Error); ok {
 				http.Error(w, e.Error(), e.Code)
@@ -224,7 +241,14 @@ func (h *httpCache) CacheHandler(w http.ResponseWriter, r *http.Request) {
 		defer rdr.Close()
 
 		w.Header().Set("Content-Type", "application/octet-stream")
-		w.Header().Set("Content-Length", strconv.FormatInt(sizeBytes, 10))
+		if zstdCompressed {
+			// TODO: calculate Content-Length for compressed blobs too
+			// (unless compressing on the fly).
+			w.Header().Set("Content-Encoding", "zstd")
+		} else {
+			w.Header().Set("Content-Length", strconv.FormatInt(sizeBytes, 10))
+		}
+
 		io.Copy(w, rdr)
 
 		h.logResponse(http.StatusOK, r)
@@ -232,6 +256,22 @@ func (h *httpCache) CacheHandler(w http.ResponseWriter, r *http.Request) {
 	case http.MethodPut:
 		contentLength := r.ContentLength
 
+		// If custom header X-Digest-SizeBytes is set, use that for the
+		// size of the blob instead of Content-Length (which only works
+		// for uncompressed PUTs).
+		sb := r.Header.Get("X-Digest-SizeBytes")
+		if sb != "" {
+			cl, err := strconv.Atoi(sb)
+			if err != nil {
+				msg := fmt.Sprintf("PUT with unparseable X-Digest-SizeBytes header: %v", sb)
+				http.Error(w, msg, http.StatusBadRequest)
+				h.errorLogger.Printf("PUT %s: %s", path(kind, hash), msg)
+				return
+			}
+
+			contentLength = int64(cl)
+		}
+
 		if contentLength == -1 {
 			// We need the content-length header to make sure we have enough disk space.
 			msg := fmt.Sprintf("PUT without Content-Length (key = %s)", path(kind, hash))
@@ -247,11 +287,24 @@ func (h *httpCache) CacheHandler(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		rc := r.Body
+		zstdCompressed := false
+
+		// Content-Encoding must be one of "identity", "zstd" or not present.
+		ce := r.Header.Get("Content-Encoding")
+		if ce == "zstd" {
+			zstdCompressed = true
+		} else if ce != "" && ce != "identity" {
+			msg := fmt.Sprintf("Unsupported content-encoding: %q", ce)
+			http.Error(w, msg, http.StatusBadRequest)
+			h.errorLogger.Printf("PUT %s: %s", path(kind, hash), msg)
+			return
+		}
+
+		var rdr io.Reader = r.Body
 		if h.validateAC && kind == cache.AC {
 			// verify that this is a valid ActionResult
 
-			data, err := ioutil.ReadAll(rc)
+			data, err := ioutil.ReadAll(rdr)
 			if err != nil {
 				msg := "failed to read request body"
 				http.Error(w, msg, http.StatusInternalServerError)
@@ -259,6 +312,19 @@ func (h *httpCache) CacheHandler(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 
+			if zstdCompressed {
+				uncompressed, err := decoder.DecodeAll(data, nil)
+				if err != nil {
+					msg := fmt.Sprintf("failed to uncompress zstd-encoded request body: %v", err)
+					http.Error(w, msg, http.StatusBadRequest)
+					h.errorLogger.Printf("PUT %s: %s", path(kind, hash), msg)
+					return
+				}
+
+				data = uncompressed
+				zstdCompressed = false
+			}
+
 			if int64(len(data)) != contentLength {
 				msg := fmt.Sprintf("sizes don't match. Expected %d, found %d",
 					contentLength, len(data))
@@ -279,10 +345,32 @@ func (h *httpCache) CacheHandler(w http.ResponseWriter, r *http.Request) {
 			// Note: we do not currently verify that the blobs exist
 			// in the CAS.
 
-			rc = ioutil.NopCloser(bytes.NewReader(data))
+			rdr = bytes.NewReader(data)
+		}
+
+		if zstdCompressed {
+			z, ok := decoderPool.Get().(*syncpool.DecoderWrapper)
+			if !ok {
+				err = errDecoderPoolFail
+			} else {
+				err = z.Reset(rdr)
+			}
+			if err != nil {
+				if z != nil {
+					defer z.Close()
+				}
+				msg := fmt.Sprintf("Failed to create zstd reader: %v", err)
+				http.Error(w, msg, http.StatusInternalServerError)
+				h.errorLogger.Printf("PUT %s: %s", path(kind, hash), msg)
+				return
+			}
+
+			rc := z.IOReadCloser()
+			defer rc.Close()
+			rdr = rc
 		}
 
-		err := h.cache.Put(kind, hash, contentLength, rc)
+		err := h.cache.Put(kind, hash, contentLength, rdr)
 		if err != nil {
 			if cerr, ok := err.(*cache.Error); ok {
 				http.Error(w, err.Error(), cerr.Code)
@@ -360,7 +448,7 @@ func addWorkerMetadataHTTP(addr string, ct string, orig []byte) (data []byte, co
 func (h *httpCache) StatusPageHandler(w http.ResponseWriter, r *http.Request) {
 	defer r.Body.Close()
 
-	totalSize, reservedSize, numItems := h.cache.Stats()
+	totalSize, reservedSize, numItems, uncompressedSize := h.cache.Stats()
 
 	goroutines := runtime.NumGoroutine()
 
@@ -368,13 +456,14 @@ func (h *httpCache) StatusPageHandler(w http.ResponseWriter, r *http.Request) {
 	enc := json.NewEncoder(w)
 	enc.SetIndent("", " ")
 	enc.Encode(statusPageData{
-		MaxSize:       h.cache.MaxSize(),
-		CurrSize:      totalSize,
-		ReservedSize:  reservedSize,
-		NumFiles:      numItems,
-		ServerTime:    time.Now().Unix(),
-		GitCommit:     h.gitCommit,
-		NumGoroutines: goroutines,
+		MaxSize:          h.cache.MaxSize(),
+		CurrSize:         totalSize,
+		UncompressedSize: uncompressedSize,
+		ReservedSize:     reservedSize,
+		NumFiles:         numItems,
+		ServerTime:       time.Now().Unix(),
+		GitCommit:        h.gitCommit,
+		NumGoroutines:    goroutines,
 	})
 }
 
diff --git a/utils/tempfile/tempfile.go b/utils/tempfile/tempfile.go
index 370b1fa..3efecbb 100644
--- a/utils/tempfile/tempfile.go
+++ b/utils/tempfile/tempfile.go
@@ -33,25 +33,38 @@ func (c *Creator) ranqd1() string {
 }
 
 const flags = os.O_RDWR | os.O_CREATE | os.O_EXCL
-const mode = 0666 // Before the umask is applied.
 
-// Create attempts to create a temp file for eventualFile, and returns
-// the corresponding *os.File and an error if any occurred. The temp
-// file is created in the same directory as eventualFile, with permissions
-// 0666 before the umask is applied.
-func (c *Creator) Create(eventualFile string) (*os.File, error) {
+const EndMode = 0666
+const wipMode = EndMode | os.ModeSetgid
+
+// Create attempts to create a file whose name is of the form
+// <base>-<randomstring> and with a ".v1" suffix if `legacy` is
+// true. The file will be created with the setgid bit set, which
+// indicates that it is not complete. The *os.File is returned
+// along with the random string, and an error if something went
+// wrong.
+//
+// Once the file has been successfully written by the caller, it
+// should be chmod'ed to `EndMode` to mark it as complete.
+func (c *Creator) Create(base string, legacy bool) (*os.File, string, error) {
 	var err error
 	var f *os.File
-
-	base := eventualFile + ".tmp"
+	var name string
+	var random string
 
 	for i := 0; i < 10000; i++ {
-		name := base + c.ranqd1()
-		f, err = os.OpenFile(name, flags, mode)
+		random = c.ranqd1()
+		if legacy {
+			name = base + "-" + random + ".v1"
+		} else {
+			name = base + "-" + random
+		}
+
+		f, err = os.OpenFile(name, flags, wipMode)
 		if os.IsExist(err) {
 			continue
 		}
-		return f, err
+		return f, random, err
 	}
-	return nil, err
+	return nil, "", err
 }
diff --git a/utils/zstdpool/BUILD.bazel b/utils/zstdpool/BUILD.bazel
new file mode 100644
index 0000000..ee576a2
--- /dev/null
+++ b/utils/zstdpool/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["zstdpool.go"],
+    importpath = "github.com/buchgr/bazel-remote/utils/zstdpool",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@com_github_klauspost_compress//zstd:go_default_library",
+        "@com_github_mostynb_zstdpool_syncpool//:go_default_library",
+    ],
+)
diff --git a/utils/zstdpool/zstdpool.go b/utils/zstdpool/zstdpool.go
new file mode 100644
index 0000000..15cdea6
--- /dev/null
+++ b/utils/zstdpool/zstdpool.go
@@ -0,0 +1,33 @@
+package zstdpool
+
+import (
+	"sync"
+
+	"github.com/klauspost/compress/zstd"
+	syncpool "github.com/mostynb/zstdpool-syncpool"
+)
+
+var onceEncPool sync.Once
+var encoderPool *sync.Pool
+
+var onceDecPool sync.Once
+var decoderPool *sync.Pool
+
+func GetEncoderPool() *sync.Pool {
+	onceEncPool.Do(func() {
+		encoderPool = syncpool.NewEncoderPool(
+			zstd.WithEncoderConcurrency(1),
+			zstd.WithEncoderLevel(zstd.SpeedFastest))
+	})
+
+	return encoderPool
+}
+
+func GetDecoderPool() *sync.Pool {
+	onceDecPool.Do(func() {
+		decoderPool = syncpool.NewDecoderPool(
+			zstd.WithDecoderConcurrency(1))
+	})
+
+	return decoderPool
+}


Test patch:
diff --git a/cache/disk/casblob/casblob_test.go b/cache/disk/casblob/casblob_test.go
new file mode 100644
index 0000000..b3e39af
--- /dev/null
+++ b/cache/disk/casblob/casblob_test.go
@@ -0,0 +1,15 @@
+package casblob_test
+
+import (
+	"testing"
+	"unsafe"
+)
+
+func TestLenSize(t *testing.T) {
+	slice := []int{}
+	if unsafe.Sizeof(len(slice)) > 8 {
+		// If this fails, then we have a bunch of potential truncation
+		// errors all over the place.
+		t.Errorf("len() returns a value larger than 8 bytes")
+	}
+}
diff --git a/cache/disk/disk_test.go b/cache/disk/disk_test.go
index a5378dc..9fb1b4a 100644
--- a/cache/disk/disk_test.go
+++ b/cache/disk/disk_test.go
@@ -18,6 +18,7 @@ import (
 	"time"
 
 	"github.com/buchgr/bazel-remote/cache"
+	"github.com/buchgr/bazel-remote/cache/disk/casblob"
 	"github.com/buchgr/bazel-remote/cache/httpproxy"
 	testutils "github.com/buchgr/bazel-remote/utils"
 
@@ -43,9 +44,11 @@ func TestCacheBasics(t *testing.T) {
 	defer os.RemoveAll(cacheDir)
 
 	itemSize := int64(256)
-	cacheSize := itemSize
 
-	testCache, err := New(cacheDir, cacheSize, nil)
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := int64(itemSize * 2)
+
+	testCache, err := New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -58,7 +61,7 @@ func TestCacheBasics(t *testing.T) {
 	data, hash := testutils.RandomDataAndHash(itemSize)
 
 	// Non-existing item.
-	rdr, _, err := testCache.Get(cache.CAS, hash, itemSize)
+	rdr, _, err := testCache.Get(cache.CAS, hash, itemSize, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -74,7 +77,7 @@ func TestCacheBasics(t *testing.T) {
 	}
 
 	// Get the item back.
-	rdr, sizeBytes, err := testCache.Get(cache.CAS, hash, itemSize)
+	rdr, sizeBytes, err := testCache.Get(cache.CAS, hash, itemSize, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -88,7 +91,7 @@ func TestCacheBasics(t *testing.T) {
 func TestCachePutWrongSize(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, 100, nil)
+	testCache, err := New(cacheDir, 100, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -115,7 +118,7 @@ func TestCacheGetContainsWrongSize(t *testing.T) {
 
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, 100, nil)
+	testCache, err := New(cacheDir, 100, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -133,7 +136,7 @@ func TestCacheGetContainsWrongSize(t *testing.T) {
 		t.Error("Expected not found, due to size being different")
 	}
 
-	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, contentsLength+1)
+	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, contentsLength+1, 0)
 	if rdr != nil {
 		t.Error("Expected not found, due to size being different")
 	}
@@ -143,7 +146,7 @@ func TestCacheGetContainsWrongSize(t *testing.T) {
 		t.Error("Expected found, when unknown size")
 	}
 
-	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, -1)
+	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, -1, 0)
 	if rdr == nil {
 		t.Error("Expected found, when unknown size")
 	}
@@ -153,7 +156,7 @@ func TestCacheGetContainsWrongSizeWithProxy(t *testing.T) {
 
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, 100, new(proxyStub))
+	testCache, err := New(cacheDir, 100, "zstd", new(proxyStub))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -173,7 +176,7 @@ func TestCacheGetContainsWrongSizeWithProxy(t *testing.T) {
 		t.Fatal("Expected not found, due to size being different")
 	}
 
-	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, contentsLength+1)
+	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, contentsLength+1, 0)
 	if rdr != nil {
 		t.Fatal("Expected not found, due to size being different")
 	}
@@ -188,7 +191,7 @@ func TestCacheGetContainsWrongSizeWithProxy(t *testing.T) {
 		t.Fatal("Expected found, when unknown size")
 	}
 
-	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, -1)
+	rdr, _, _ = testCache.Get(cache.CAS, contentsHash, -1, 0)
 	if rdr == nil {
 		t.Fatal("Expected found, when unknown size")
 	}
@@ -203,18 +206,39 @@ func TestCacheGetContainsWrongSizeWithProxy(t *testing.T) {
 // digest {contentsHash, contentsLength}.
 type proxyStub struct{}
 
-func (d proxyStub) Put(kind cache.EntryKind, hash string, size int64, rc io.ReadCloser) {}
+func (d proxyStub) Put(kind cache.EntryKind, hash string, size int64, rc io.ReadCloser) {
+	// Not implemented.
+}
 
 func (d proxyStub) Get(kind cache.EntryKind, hash string) (io.ReadCloser, int64, error) {
-	if hash != contentsHash {
+	if hash != contentsHash || kind != cache.CAS {
 		return nil, -1, nil
 	}
 
-	return ioutil.NopCloser(strings.NewReader(contents)), contentsLength, nil
+	tmpfile, err := ioutil.TempFile("", "proxyStubGet")
+	if err != nil {
+		return nil, -1, err
+	}
+	tfn := tmpfile.Name()
+	defer os.Remove(tfn)
+
+	_, err = casblob.WriteAndClose(ioutil.NopCloser(
+		strings.NewReader(contents)), tmpfile, casblob.Zstandard,
+		hash, contentsLength)
+	if err != nil {
+		return nil, -1, err
+	}
+
+	readme, err := os.Open(tfn)
+	if err != nil {
+		return nil, -1, err
+	}
+
+	return readme, contentsLength, nil
 }
 
 func (d proxyStub) Contains(kind cache.EntryKind, hash string) (bool, int64) {
-	if hash != contentsHash {
+	if hash != contentsHash || kind != cache.CAS {
 		return false, -1
 	}
 
@@ -254,7 +278,7 @@ func putGetCompareBytes(kind cache.EntryKind, hash string, data []byte, testCach
 		return err
 	}
 
-	rdr, sizeBytes, err := testCache.Get(kind, hash, int64(len(data)))
+	rdr, sizeBytes, err := testCache.Get(kind, hash, int64(len(data)), 0)
 	if err != nil {
 		return err
 	}
@@ -271,7 +295,8 @@ func hashStr(content string) string {
 func TestOverwrite(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, 10, nil)
+
+	testCache, err := New(cacheDir, 100, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -311,27 +336,32 @@ func TestCacheExistingFiles(t *testing.T) {
 	items := []struct {
 		contents string
 		hash     string
+		key      string
 		file     string
 	}{
 		{
 			"hej",
 			"9c478bf63e9500cb5db1e85ece82f18c8eb9e52e2f9135acd7f10972c8d563ba",
-			"cas/9c/9c478bf63e9500cb5db1e85ece82f18c8eb9e52e2f9135acd7f10972c8d563ba",
+			"cas/9c478bf63e9500cb5db1e85ece82f18c8eb9e52e2f9135acd7f10972c8d563ba",
+			"cas.v2/9c/9c478bf63e9500cb5db1e85ece82f18c8eb9e52e2f9135acd7f10972c8d563ba-3-123456789",
 		},
 		{
 			"världen",
 			"d497feaa39156f4ae61317db9d2adc3a8f2ff1437fd48ccb56f814f0b7ac5fe1",
-			"cas/d4/d497feaa39156f4ae61317db9d2adc3a8f2ff1437fd48ccb56f814f0b7ac5fe1",
+			"cas/d497feaa39156f4ae61317db9d2adc3a8f2ff1437fd48ccb56f814f0b7ac5fe1",
+			"cas.v2/d4/d497feaa39156f4ae61317db9d2adc3a8f2ff1437fd48ccb56f814f0b7ac5fe1-8-123456789",
 		},
 		{
 			"foo",
 			"733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54",
-			"ac/73/733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54",
+			"ac/733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54",
+			"ac/73/733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54-123456789",
 		},
 		{
 			"bar",
 			"733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54",
-			"raw/73/733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54",
+			"raw/733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54",
+			"raw/73/733e21b37cef883579a88183eed0d00cdeea0b59e1bcd77db6957f881c3a6b54-123456789",
 		},
 	}
 
@@ -341,7 +371,17 @@ func TestCacheExistingFiles(t *testing.T) {
 		fp := path.Join(cacheDir, it.file)
 		ensureDirExists(path.Dir(fp))
 
-		err = ioutil.WriteFile(fp, []byte(it.contents), os.ModePerm)
+		if strings.HasPrefix(it.file, "cas.v2/") {
+			r := bytes.NewReader([]byte(it.contents))
+			var f *os.File
+			f, err = os.Create(fp)
+			if err == nil {
+				_, err = casblob.WriteAndClose(r, f, casblob.Zstandard,
+					it.hash, int64(len(it.contents)))
+			}
+		} else {
+			err = ioutil.WriteFile(fp, []byte(it.contents), os.ModePerm)
+		}
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -350,7 +390,10 @@ func TestCacheExistingFiles(t *testing.T) {
 		time.Sleep(10 * time.Millisecond)
 	}
 
-	testCache, err := New(cacheDir, 1024, nil)
+	// Add some overhead for likely CAS blob storage expansion.
+	const cacheSize = 1024
+
+	testCache, err := New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -386,9 +429,9 @@ func TestCacheExistingFiles(t *testing.T) {
 			continue
 		}
 
-		if evicted[0] != items[0].file {
+		if evicted[0] != items[0].key {
 			t.Fatalf("Expected first evicted item to be %s, was %s",
-				items[0].file, evicted[0])
+				items[0].key, evicted[0])
 		}
 
 		break // First item evicted as expected.
@@ -405,7 +448,7 @@ func TestCacheExistingFiles(t *testing.T) {
 func TestCacheBlobTooLarge(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, 100, nil)
+	testCache, err := New(cacheDir, 100, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -431,7 +474,7 @@ func TestCacheBlobTooLarge(t *testing.T) {
 func TestCacheCorruptedCASBlob(t *testing.T) {
 	cacheDir := tempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err := New(cacheDir, 1000, nil)
+	testCache, err := New(cacheDir, 1000, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -460,28 +503,67 @@ func createRandomFile(dir string, size int64) (string, error) {
 	return hash, ioutil.WriteFile(filepath, data, os.ModePerm)
 }
 
+func createRandomV1CASFile(dir string, size int64) (string, error) {
+	data, hash := testutils.RandomDataAndHash(size)
+	os.MkdirAll(dir, os.ModePerm)
+	filePath := dir + "/" + hash
+
+	err := ioutil.WriteFile(filePath, data, os.ModePerm)
+	if err != nil {
+		return "", err
+	}
+
+	return hash, nil
+}
+
+func createRandomCASFile(dir string, size int64) (string, error) {
+	data, hash := testutils.RandomDataAndHash(size)
+	os.MkdirAll(dir, os.ModePerm)
+	filePath := dir + "/" + hash
+
+	f, err := os.Create(filePath)
+	if err != nil {
+		return "", nil
+	}
+
+	r := bytes.NewReader(data)
+
+	_, err = casblob.WriteAndClose(r, f, casblob.Zstandard, hash, int64(len(data)))
+	if err != nil {
+		return "", nil
+	}
+
+	return hash, nil
+}
+
 func TestMigrateFromOldDirectoryStructure(t *testing.T) {
 	cacheDir := testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir)
 
-	acHash, err := createRandomFile(cacheDir+"/ac/", 512)
+	acHash, err := createRandomFile(cacheDir+"/ac", 512)
 	if err != nil {
 		t.Fatal(err)
 	}
-	casHash1, err := createRandomFile(cacheDir+"/cas/", 1024)
+
+	casHash1, err := createRandomV1CASFile(cacheDir+"/cas", 1024)
 	if err != nil {
 		t.Fatal(err)
 	}
-	casHash2, err := createRandomFile(cacheDir+"/cas/", 1024)
+
+	casHash2, err := createRandomV1CASFile(cacheDir+"/cas", 1024)
 	if err != nil {
 		t.Fatal(err)
 	}
-	testCache, err := New(cacheDir, 2560, nil)
+
+	// Add some overhead for likely CAS blob storage expansion.
+	const cacheSize = 2560 * 2
+
+	testCache, err := New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	_, _, numItems := testCache.Stats()
+	_, _, numItems, _ := testCache.Stats()
 	if numItems != 3 {
 		t.Fatalf("Expected test cache size 3 but was %d", numItems)
 	}
@@ -511,25 +593,47 @@ func TestLoadExistingEntries(t *testing.T) {
 	numBlobs := int64(3)
 	blobSize := int64(1024)
 
-	acHash, err := testutils.CreateCacheFile(cacheDir+"/ac/", blobSize)
+	var err error
+
+	acData, acHash := testutils.RandomDataAndHash(blobSize)
+	err = os.MkdirAll(path.Join(cacheDir, "ac"), 0755)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = ioutil.WriteFile(path.Join(cacheDir, "ac", acHash), acData, 0644)
 	if err != nil {
 		t.Fatal(err)
 	}
-	casHash, err := testutils.CreateCacheFile(cacheDir+"/cas/", blobSize)
+
+	casData, casHash := testutils.RandomDataAndHash(blobSize)
+	err = os.MkdirAll(path.Join(cacheDir, "cas"), 0755)
 	if err != nil {
 		t.Fatal(err)
 	}
-	rawHash, err := testutils.CreateCacheFile(cacheDir+"/raw/", blobSize)
+	err = ioutil.WriteFile(path.Join(cacheDir, "cas", casHash), casData, 0644)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	testCache, err := New(cacheDir, blobSize*numBlobs, nil)
+	rawData, rawHash := testutils.RandomDataAndHash(blobSize)
+	err = os.MkdirAll(path.Join(cacheDir, "raw"), 0755)
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = ioutil.WriteFile(path.Join(cacheDir, "raw", rawHash), rawData, 0644)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := int64(blobSize * numBlobs * 2)
+
+	testCache, err := New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	_, _, numItems := testCache.Stats()
+	_, _, numItems, _ := testCache.Stats()
 	if int64(numItems) != numBlobs {
 		t.Fatalf("Expected test cache size %d but was %d",
 			numBlobs, numItems)
@@ -558,9 +662,11 @@ func TestDistinctKeyspaces(t *testing.T) {
 	defer os.RemoveAll(cacheDir)
 
 	blobSize := 1024
-	cacheSize := int64(blobSize * 3)
 
-	testCache, err := New(cacheDir, cacheSize, nil)
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := int64(blobSize*3) * 2
+
+	testCache, err := New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -586,7 +692,7 @@ func TestDistinctKeyspaces(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	_, _, numItems := testCache.Stats()
+	_, _, numItems, _ := testCache.Stats()
 	if numItems != 3 {
 		t.Fatalf("Expected test cache size 3 but was %d",
 			numItems)
@@ -669,14 +775,18 @@ func TestHttpProxyBackend(t *testing.T) {
 	accessLogger := testutils.NewSilentLogger()
 	errorLogger := testutils.NewSilentLogger()
 
-	proxy := httpproxy.New(url, &http.Client{}, accessLogger, errorLogger, 100, 1000000)
+	proxy, err := httpproxy.New(url, "zstd", &http.Client{}, accessLogger, errorLogger, 100, 1000000)
+	if err != nil {
+		t.Fatal(err)
+	}
 
 	cacheDir := testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir)
 
-	cacheSize := int64(1024 * 10)
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := int64(1024*10) * 2
 
-	testCache, err := New(cacheDir, cacheSize, proxy)
+	testCache, err := New(cacheDir, cacheSize, "zstd", proxy)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -685,7 +795,7 @@ func TestHttpProxyBackend(t *testing.T) {
 	blob, casHash := testutils.RandomDataAndHash(blobSize)
 
 	// Non-existing item
-	r, _, err := testCache.Get(cache.CAS, casHash, blobSize)
+	r, _, err := testCache.Get(cache.CAS, casHash, blobSize, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -714,7 +824,7 @@ func TestHttpProxyBackend(t *testing.T) {
 	// Create a new (empty) testCache, without a proxy backend.
 	cacheDir = testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir)
-	testCache, err = New(cacheDir, cacheSize, nil)
+	testCache, err = New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -727,7 +837,7 @@ func TestHttpProxyBackend(t *testing.T) {
 		t.Fatalf("Expected the cache not to contain %s", casHash)
 	}
 
-	r, _, err = testCache.Get(cache.CAS, casHash, blobSize)
+	r, _, err = testCache.Get(cache.CAS, casHash, blobSize, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -744,7 +854,7 @@ func TestHttpProxyBackend(t *testing.T) {
 			casHash)
 	}
 
-	r, fetchedSize, err := testCache.Get(cache.CAS, casHash, blobSize)
+	r, fetchedSize, err := testCache.Get(cache.CAS, casHash, blobSize, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -776,7 +886,7 @@ func TestGetValidatedActionResult(t *testing.T) {
 	cacheDir := testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir)
 
-	testCache, err := New(cacheDir, 1024*32, nil)
+	testCache, err := New(cacheDir, 1024*32, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -941,3 +1051,64 @@ func TestGetValidatedActionResult(t *testing.T) {
 		t.Fatal("Returned ActionResult proto does not match")
 	}
 }
+
+func TestGetWithOffset(t *testing.T) {
+	cacheDir := testutils.TempDir(t)
+	defer os.RemoveAll(cacheDir)
+
+	const blobSize = 2048 + 256
+
+	testCache, err := New(cacheDir, blobSize*2, "zstd", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	data, hash := testutils.RandomDataAndHash(blobSize)
+
+	err = testCache.Put(cache.CAS, hash, blobSize,
+		ioutil.NopCloser(bytes.NewReader(data)))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	rc, foundSize, err := testCache.Get(cache.CAS, hash, int64(len(data)), 0)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if foundSize != int64(len(data)) {
+		t.Fatalf("Got back a blob with size %d, original blob had size %d",
+			foundSize, int64(len(data)))
+	}
+
+	// Read back the full blob, confirm the test state is OK.
+	foundData, err := ioutil.ReadAll(rc)
+	if err != nil {
+		t.Fatal(err)
+	}
+	rc.Close()
+	if !bytes.Equal(data, foundData) {
+		t.Fatal("Got back different data")
+	}
+
+	// Now try some partial reads.
+	for _, offset := range []int64{42, 1023, 1024, 1025, 2048, 2303} {
+		rc, foundSize, err = testCache.Get(cache.CAS, hash, int64(len(data)), offset)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if foundSize != int64(len(data)) {
+			t.Fatalf("Got back a blob with size %d, original blob had size %d",
+				foundSize, int64(len(data)))
+		}
+
+		foundData, err = ioutil.ReadAll(rc)
+		if err != nil {
+			t.Fatal(err)
+		}
+		rc.Close()
+		if !bytes.Equal(data[offset:], foundData) {
+			t.Fatalf("Expected data (%d bytes), differs from actual data (%d bytes) for offset %d",
+				len(data[offset:]), len(foundData), offset)
+		}
+	}
+}
diff --git a/cache/disk/lru_test.go b/cache/disk/lru_test.go
index f482fd3..bb48c9a 100644
--- a/cache/disk/lru_test.go
+++ b/cache/disk/lru_test.go
@@ -36,7 +36,7 @@ func TestBasics(t *testing.T) {
 
 	// Add an item
 	aKey := "akey"
-	anItem := lruItem{size: 5}
+	anItem := lruItem{size: 5, sizeOnDisk: 5}
 	ok = lru.Add(aKey, anItem)
 	if !ok {
 		t.Fatalf("Add: failed inserting item")
@@ -85,7 +85,7 @@ func TestEviction(t *testing.T) {
 	var expectedEvictions []int
 
 	for i, thisExpected := range expectedSizesNumItems {
-		item := lruItem{size: int64(i)}
+		item := lruItem{size: int64(i), sizeOnDisk: int64(i)}
 		ok := lru.Add(i, item)
 		if !ok {
 			t.Fatalf("Add: failed adding %d", i)
@@ -104,7 +104,7 @@ func TestRejectBigItem(t *testing.T) {
 	// Bounded caches should reject big items
 	lru := NewSizedLRU(10, nil)
 
-	ok := lru.Add("hello", lruItem{size: 11})
+	ok := lru.Add("hello", lruItem{size: 11, sizeOnDisk: 11})
 	if ok {
 		t.Fatalf("Add succeeded, expected it to fail")
 	}
@@ -113,7 +113,7 @@ func TestRejectBigItem(t *testing.T) {
 }
 
 func TestReserveZeroAlwaysPossible(t *testing.T) {
-	largeItem := lruItem{size: math.MaxInt64}
+	largeItem := lruItem{size: math.MaxInt64, sizeOnDisk: math.MaxInt64}
 
 	lru := NewSizedLRU(math.MaxInt64, nil)
 	lru.Add("foo", largeItem)
@@ -256,7 +256,7 @@ func TestAddWithSpaceReserved(t *testing.T) {
 		t.Fatalf("Expected to be able to reserve 1")
 	}
 
-	ok = lru.Add("hello", lruItem{size: 2})
+	ok = lru.Add("hello", lruItem{size: 2, sizeOnDisk: 2})
 	if ok {
 		t.Fatal("Expected to not be able to add item with size 2")
 	}
@@ -266,7 +266,7 @@ func TestAddWithSpaceReserved(t *testing.T) {
 		t.Fatal("Expected to be able to unreserve 1:", err)
 	}
 
-	ok = lru.Add("hello", lruItem{size: 2})
+	ok = lru.Add("hello", lruItem{size: 2, sizeOnDisk: 2})
 	if !ok {
 		t.Fatal("Expected to be able to add item with size 2")
 	}
diff --git a/cache/httpproxy/httpproxy_test.go b/cache/httpproxy/httpproxy_test.go
index 18ae7a7..49ec6a8 100644
--- a/cache/httpproxy/httpproxy_test.go
+++ b/cache/httpproxy/httpproxy_test.go
@@ -2,6 +2,7 @@ package httpproxy
 
 import (
 	"bytes"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
@@ -17,6 +18,7 @@ import (
 
 	"github.com/buchgr/bazel-remote/cache"
 	"github.com/buchgr/bazel-remote/cache/disk"
+	"github.com/buchgr/bazel-remote/cache/disk/casblob"
 	testutils "github.com/buchgr/bazel-remote/utils"
 )
 
@@ -32,11 +34,16 @@ func (s *testServer) handler(w http.ResponseWriter, r *http.Request) {
 
 	fields := strings.Split(r.URL.Path, "/")
 
+	kind := fields[1]
 	kindMap := s.ac
-	if fields[1] == "ac" {
+	if kind == "ac" {
 		kindMap = s.ac
-	} else if fields[1] == "cas" {
+	} else if kind == "cas.v2" {
 		kindMap = s.cas
+	} else {
+		msg := fmt.Sprintf("unsupported URL: %q", r.URL.Path)
+		http.Error(w, msg, http.StatusBadRequest)
+		return
 	}
 	hash := fields[2]
 
@@ -101,9 +108,13 @@ func TestEverything(t *testing.T) {
 	t.Log("cas HASH:", hash)
 	acData := []byte{1, 2, 3, 4}
 
-	proxyCache := New(url, &http.Client{}, accessLogger, errorLogger, 100, 10000)
-	diskCacheSize := int64(len(casData) + 1024)
-	diskCache, err := disk.New(cacheDir, diskCacheSize, proxyCache)
+	proxyCache, err := New(url, "zstd", &http.Client{}, accessLogger, errorLogger, 100, 10000)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	diskCacheSize := int64(len(casData) + 2048)
+	diskCache, err := disk.New(cacheDir, diskCacheSize, "zstd", proxyCache)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -148,11 +159,43 @@ func TestEverything(t *testing.T) {
 			t.Fatal("Proxied AC value does not match")
 		}
 	}
+
 	for _, v := range s.cas {
-		if !bytes.Equal(v, casData) {
+
+		// TODO: tweak the GetUncompressedReadCloser API to accept more than os.File.
+
+		tmpfile, err := ioutil.TempFile("", "bazel-remote-httpproxy-test")
+		if err != nil {
+			t.Fatal(err)
+		}
+		tfn := tmpfile.Name()
+		defer os.Remove(tfn)
+
+		_, err = io.Copy(tmpfile, bytes.NewReader(v))
+		if err != nil {
+			t.Fatal(err)
+		}
+		tmpfile2, err := os.Open(tfn)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer os.Remove(tmpfile2.Name())
+
+		rc, err := casblob.GetUncompressedReadCloser(tmpfile2, int64(len(casData)), 0)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer rc.Close()
+		vData, err := ioutil.ReadAll(rc)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if !bytes.Equal(vData, casData) {
 			t.Fatal("Proxied CAS value does not match")
 		}
 	}
+
 	s.mu.Unlock()
 
 	// Confirm that we can HEAD both values successfully.
@@ -183,7 +226,7 @@ func TestEverything(t *testing.T) {
 	var data []byte
 	var rc io.ReadCloser
 
-	rc, size, err = diskCache.Get(cache.AC, hash, int64(len(acData)))
+	rc, size, err = diskCache.Get(cache.AC, hash, int64(len(acData)), 0)
 	if err != nil {
 		t.Error(err)
 	}
@@ -203,7 +246,7 @@ func TestEverything(t *testing.T) {
 	}
 	rc.Close()
 
-	rc, size, err = diskCache.Get(cache.CAS, hash, int64(len(casData)))
+	rc, size, err = diskCache.Get(cache.CAS, hash, int64(len(casData)), 0)
 	if err != nil {
 		t.Error(err)
 	}
@@ -229,12 +272,12 @@ func TestEverything(t *testing.T) {
 	cacheDir2 := testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir2)
 
-	diskCache, err = disk.New(cacheDir2, diskCacheSize, proxyCache)
+	diskCache, err = disk.New(cacheDir2, diskCacheSize, "zstd", proxyCache)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	_, _, numItems := diskCache.Stats()
+	_, _, numItems, _ := diskCache.Stats()
 	if numItems != 0 {
 		t.Fatalf("Expected an empty disk cache, found %d items", numItems)
 	}
@@ -254,14 +297,14 @@ func TestEverything(t *testing.T) {
 	if !found {
 		t.Fatalf("Expected to find CAS item %s", hash)
 	}
-	if size != int64(len(casData)) {
+	if size != -1 && size != int64(len(casData)) {
 		t.Fatalf("Expected to find CAS item with size %d, got %d",
 			len(casData), size)
 	}
 
 	// Confirm that we can GET both values successfully.
 
-	rc, size, err = diskCache.Get(cache.AC, hash, int64(len(acData)))
+	rc, size, err = diskCache.Get(cache.AC, hash, int64(len(acData)), 0)
 	if err != nil {
 		t.Error(err)
 	}
@@ -281,7 +324,7 @@ func TestEverything(t *testing.T) {
 	}
 	rc.Close()
 
-	rc, size, err = diskCache.Get(cache.CAS, hash, int64(len(casData)))
+	rc, size, err = diskCache.Get(cache.CAS, hash, int64(len(casData)), 0)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/cache/s3proxy/s3proxy_test.go b/cache/s3proxy/s3proxy_test.go
index 30a5245..b87f134 100644
--- a/cache/s3proxy/s3proxy_test.go
+++ b/cache/s3proxy/s3proxy_test.go
@@ -1,48 +1,38 @@
 package s3proxy
 
 import (
-	"log"
 	"testing"
 
 	"github.com/buchgr/bazel-remote/cache"
 )
 
 func TestObjectKey(t *testing.T) {
-	result, expected := "", ""
-	logger := &log.Logger{}
-	tc := &s3Cache{
-		mcore:        nil,
-		bucket:       "test",
-		keyVersion:   1,
-		uploadQueue:  make(chan uploadReq, 1),
-		accessLogger: logger,
-		errorLogger:  logger,
+	testCases := []struct {
+		prefix     string
+		key        string
+		kind       cache.EntryKind
+		expectedV1 string
+		expectedV2 string
+	}{
+		{"", "1234", cache.CAS, "cas/12/1234", "cas.v2/12/1234"},
+		{"test", "1234", cache.CAS, "test/cas/12/1234", "test/cas.v2/12/1234"},
+		{"foo/bar/grok", "1234", cache.CAS, "foo/bar/grok/cas/12/1234", "foo/bar/grok/cas.v2/12/1234"},
+		{"", "1234", cache.AC, "ac/12/1234", "ac/12/1234"},
+		{"", "1234", cache.RAW, "raw/12/1234", "raw/12/1234"},
+		{"foo/bar", "1234", cache.AC, "foo/bar/ac/12/1234", "foo/bar/ac/12/1234"},
 	}
 
-	// Legacy key format tests
-	tc.keyVersion = 1
-	tc.prefix = ""
-	result, expected = tc.objectKey("1234", cache.CAS), "cas/1234"
-	checkTestCase(t, result, expected, "legacy format without prefix")
-
-	tc.prefix = "test"
-	result, expected = tc.objectKey("1234", cache.CAS), "test/cas/1234"
-	checkTestCase(t, result, expected, "legacy format with prefix")
-
-	// New key format tests
-	tc.keyVersion = 2
-
-	tc.prefix = ""
-	result, expected = tc.objectKey("1234", cache.CAS), "cas/12/1234"
-	checkTestCase(t, result, expected, "new format with prefix")
-
-	tc.prefix = "test"
-	result, expected = tc.objectKey("1234", cache.CAS), "test/cas/12/1234"
-	checkTestCase(t, result, expected, "new format with prefix")
-}
-
-func checkTestCase(t *testing.T, result string, expected string, testCase string) {
-	if result != expected {
-		t.Errorf("%s objectKey did not match. (result: '%s' expected: '%s'", testCase, result, expected)
+	for _, tc := range testCases {
+		result := objectKeyV2(tc.prefix, tc.key, tc.kind)
+		if result != tc.expectedV2 {
+			t.Errorf("objectKeyV2 did not match. (result: '%s' expected: '%s'",
+				result, tc.expectedV2)
+		}
+
+		result = objectKeyV1(tc.prefix, tc.key, tc.kind)
+		if result != tc.expectedV1 {
+			t.Errorf("objectKeyV1 did not match. (result: '%s' expected: '%s'",
+				result, tc.expectedV1)
+		}
 	}
 }
diff --git a/config/config_test.go b/config/config_test.go
index 25ef631..2644681 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -37,6 +37,7 @@ http_write_timeout: 10s
 		GRPCPort:                    9092,
 		Dir:                         "/opt/cache-dir",
 		MaxSize:                     100,
+		StorageMode:                 "zstd",
 		HtpasswdFile:                "/opt/.htpasswd",
 		TLSCertFile:                 "/opt/tls.cert",
 		TLSKeyFile:                  "/opt/tls.key",
@@ -73,11 +74,12 @@ gcs_proxy:
 	}
 
 	expectedConfig := &Config{
-		Host:     "localhost",
-		Port:     8080,
-		GRPCPort: 9092,
-		Dir:      "/opt/cache-dir",
-		MaxSize:  100,
+		Host:        "localhost",
+		Port:        8080,
+		GRPCPort:    9092,
+		Dir:         "/opt/cache-dir",
+		MaxSize:     100,
+		StorageMode: "zstd",
 		GoogleCloudStorage: &GoogleCloudStorageConfig{
 			Bucket:                "gcs-bucket",
 			UseDefaultCredentials: false,
@@ -101,6 +103,7 @@ dir: /opt/cache-dir
 max_size: 100
 http_proxy:
   url: https://remote-cache.com:8080/cache
+  mode: zstd
 `
 	config, err := newFromYaml([]byte(yaml))
 	if err != nil {
@@ -108,11 +111,12 @@ http_proxy:
 	}
 
 	expectedConfig := &Config{
-		Host:     "localhost",
-		Port:     8080,
-		GRPCPort: 9092,
-		Dir:      "/opt/cache-dir",
-		MaxSize:  100,
+		Host:        "localhost",
+		Port:        8080,
+		GRPCPort:    9092,
+		Dir:         "/opt/cache-dir",
+		MaxSize:     100,
+		StorageMode: "zstd",
 		HTTPBackend: &HTTPBackendConfig{
 			BaseURL: "https://remote-cache.com:8080/cache",
 		},
@@ -177,10 +181,11 @@ s3_proxy:
 	}
 
 	expectedConfig := &Config{
-		Host:    "localhost",
-		Port:    8080,
-		Dir:     "/opt/cache-dir",
-		MaxSize: 100,
+		Host:        "localhost",
+		Port:        8080,
+		Dir:         "/opt/cache-dir",
+		MaxSize:     100,
+		StorageMode: "zstd",
 		S3CloudStorage: &S3CloudStorageConfig{
 			Endpoint:        "minio.example.com:9000",
 			Bucket:          "test-bucket",
@@ -216,6 +221,7 @@ profile_port: 7070
 		Port:                   1234,
 		Dir:                    "/opt/cache-dir",
 		MaxSize:                42,
+		StorageMode:            "zstd",
 		ProfilePort:            7070,
 		ProfileHost:            "",
 		NumUploaders:           100,
@@ -247,6 +253,7 @@ func TestValidMetricsDurationBuckets(t *testing.T) {
 port: 1234
 dir: /opt/cache-dir
 max_size: 42
+storage_mode: zstd
 endpoint_metrics_duration_buckets: [.005, .1, 5]
 `
 	config, err := newFromYaml([]byte(yaml))
@@ -259,6 +266,7 @@ endpoint_metrics_duration_buckets: [.005, .1, 5]
 		Port:                   1234,
 		Dir:                    "/opt/cache-dir",
 		MaxSize:                42,
+		StorageMode:            "zstd",
 		NumUploaders:           100,
 		MaxQueuedUploads:       1000000,
 		MetricsDurationBuckets: []float64{0.005, 0.1, 5},
@@ -275,6 +283,7 @@ func TestMetricsDurationBucketsNoDupliates(t *testing.T) {
 		Port:                   8080,
 		MaxSize:                42,
 		Dir:                    "/opt/cache-dir",
+		StorageMode:            "uncompressed",
 		MetricsDurationBuckets: []float64{1, 2, 3, 3},
 	}
 	err := validateConfig(testConfig)
@@ -285,3 +294,63 @@ func TestMetricsDurationBucketsNoDupliates(t *testing.T) {
 		t.Fatal("Expected the error message to mention the invalid 'endpoint_metrics_duration_buckets' key")
 	}
 }
+
+func TestStorageModes(t *testing.T) {
+	tests := []struct {
+		yaml     string
+		expected string
+		invalid  bool
+	}{{
+		yaml: `host: localhost
+port: 1234
+dir: /foo/bar 
+max_size: 20
+`,
+		expected: "zstd",
+	},
+		{
+			yaml: `host: localhost
+port: 1234
+dir: /foo/bar 
+max_size: 20
+storage_mode: zstd
+`,
+			expected: "zstd",
+		},
+		{
+			yaml: `host: localhost
+port: 1234
+dir: /foo/bar 
+max_size: 20
+storage_mode: uncompressed
+`,
+			expected: "uncompressed",
+		},
+		{
+			yaml: `host: localhost
+port: 1234
+dir: /foo/bar 
+max_size: 20
+storage_mode: gzip
+`,
+			invalid: true,
+		}}
+
+	for _, tc := range tests {
+		cfg, err := newFromYaml([]byte(tc.yaml))
+		if !tc.invalid && err != nil {
+			t.Error("Expected to succeed, got", err)
+		}
+
+		if tc.invalid {
+			if err == nil {
+				t.Error("Expected an error, got nil")
+			}
+			continue
+		}
+
+		if cfg.StorageMode != tc.expected {
+			t.Errorf("Expected %q, got %q", tc.expected, cfg.StorageMode)
+		}
+	}
+}
diff --git a/server/grpc_bytestream.go b/server/grpc_bytestream.go
index e320d6d..5e58f3d 100644
--- a/server/grpc_bytestream.go
+++ b/server/grpc_bytestream.go
@@ -14,6 +14,12 @@ import (
 	"google.golang.org/grpc/status"
 
 	"github.com/buchgr/bazel-remote/cache"
+	"github.com/buchgr/bazel-remote/cache/disk/casblob"
+
+	"github.com/klauspost/compress/zstd"
+
+	"github.com/buchgr/bazel-remote/utils/zstdpool"
+	syncpool "github.com/mostynb/zstdpool-syncpool"
 )
 
 const (
@@ -22,16 +28,33 @@ const (
 	maxChunkSize = 2 * 1024 * 1024 // 2M
 )
 
+var decoderPool = zstdpool.GetDecoderPool()
+
 // ByteStreamServer interface:
 
 func (s *grpcServer) Read(req *bytestream.ReadRequest,
 	resp bytestream.ByteStream_ReadServer) error {
 
+	errorPrefix := "GRPC BYTESTREAM READ"
+
+	var cmp casblob.CompressionType
+	hash, size, cmp, err := s.parseReadResource(req.ResourceName, errorPrefix)
+	if err != nil {
+		return err
+	}
+
 	if req.ReadOffset < 0 {
-		s.accessLogger.Printf("GRPC BYTESTREAM READ OFFSET OUT OF RANGE: %s %d",
+		s.accessLogger.Printf("GRPC BYTESTREAM READ OFFSET INVALID: %s %d",
 			req.ReadOffset)
-		return status.Error(codes.OutOfRange,
-			"Negative ReadOffset is out of range")
+		return status.Error(codes.InvalidArgument,
+			"Negative ReadOffset is invalid")
+	}
+
+	if cmp != casblob.Identity && req.ReadLimit != 0 {
+		s.accessLogger.Printf("GRPC BYTESTREAM READ LIMIT INVALID: %s %d",
+			req.ReadLimit)
+		return status.Error(codes.InvalidArgument,
+			"ReadLimit must be 0 for compressed-blobs")
 	}
 
 	if req.ReadLimit < 0 {
@@ -41,14 +64,8 @@ func (s *grpcServer) Read(req *bytestream.ReadRequest,
 			"Negative ReadLimit is out of range")
 	}
 
-	limitedSend := req.ReadLimit != 0
-
-	errorPrefix := "GRPC BYTESTREAM READ"
-
-	hash, size, err := s.parseReadResource(req.ResourceName, errorPrefix)
-	if err != nil {
-		return err
-	}
+	limitedSend := (req.ReadLimit != 0) && cmp == casblob.Identity
+	sendLimitRemaining := req.ReadLimit
 
 	if req.ReadOffset > size {
 		msg := fmt.Sprintf("ReadOffset %d larger than expected data size %d resource: %s",
@@ -57,18 +74,29 @@ func (s *grpcServer) Read(req *bytestream.ReadRequest,
 		return status.Error(codes.OutOfRange, msg)
 	}
 
-	rdr, foundSize, err := s.cache.Get(cache.CAS, hash, size)
+	var rc io.ReadCloser
+	var foundSize int64
+
+	if cmp == casblob.Zstandard {
+		rc, foundSize, err = s.cache.GetZstd(hash, size, req.ReadOffset)
+	} else {
+		rc, foundSize, err = s.cache.Get(cache.CAS, hash, size, req.ReadOffset)
+	}
+
+	if rc != nil {
+		defer rc.Close()
+	}
+
 	if err != nil {
 		msg := fmt.Sprintf("GRPC BYTESTREAM READ FAILED: %s %v", hash, err)
 		s.accessLogger.Printf(msg)
 		return status.Error(codes.Unknown, msg)
 	}
-	if rdr == nil {
+	if rc == nil {
 		msg := fmt.Sprintf("GRPC BYTESTREAM READ BLOB NOT FOUND: %s", hash)
 		s.accessLogger.Printf(msg)
 		return status.Error(codes.NotFound, msg)
 	}
-	defer rdr.Close()
 
 	if foundSize != size {
 		// This should have been caught above.
@@ -85,15 +113,9 @@ func (s *grpcServer) Read(req *bytestream.ReadRequest,
 
 	buf := make([]byte, bufSize)
 
-	if req.ReadOffset > 0 {
-		seekReaderTo(rdr, req.ReadOffset)
-	}
-
-	sendLimitRemaining := req.ReadLimit
-
 	var chunkResp bytestream.ReadResponse
 	for {
-		n, err := rdr.Read(buf)
+		n, err := rc.Read(buf)
 
 		if n > 0 {
 			if limitedSend {
@@ -128,11 +150,13 @@ func (s *grpcServer) Read(req *bytestream.ReadRequest,
 	}
 }
 
-// Parse a ReadRequest.ResourceName, return the validated hash, size and an error.
-func (s *grpcServer) parseReadResource(name string, errorPrefix string) (string, int64, error) {
+// Parse a ReadRequest.ResourceName, return the validated hash, size, compression type and an error.
+func (s *grpcServer) parseReadResource(name string, errorPrefix string) (string, int64, casblob.CompressionType, error) {
 
 	// The resource name should be of the format:
 	// [{instance_name}]/blobs/{hash}/{size}
+	// Or:
+	// [{instance_name}]/compressed-blobs/{compressor}/{uncompressed_hash}/{uncompressed_size}
 
 	// Instance_name is ignored in this bytestream implementation, so don't
 	// bother returning it. It is not allowed to contain "blobs" as a distinct
@@ -140,41 +164,91 @@ func (s *grpcServer) parseReadResource(name string, errorPrefix string) (string,
 
 	fields := strings.Split(name, "/")
 	var rem []string
-	found := false
+	foundBlobs := false
+	foundCompressedBlobs := false
 	for i := range fields {
 		if fields[i] == "blobs" {
 			rem = fields[i+1:]
-			found = true
+			foundBlobs = true
+			break
+		}
+
+		if fields[i] == "compressed-blobs" {
+			rem = fields[i+1:]
+			foundCompressedBlobs = true
 			break
 		}
 	}
 
-	if !found || len(rem) != 2 {
+	if foundBlobs {
+		if len(rem) != 2 {
+			msg := fmt.Sprintf("Unable to parse resource name: %s", name)
+			s.accessLogger.Printf("%s: %s", errorPrefix, msg)
+			return "", 0, casblob.Identity,
+				status.Error(codes.InvalidArgument, msg)
+		}
+
+		hash := rem[0]
+
+		size, err := strconv.ParseInt(rem[1], 10, 64)
+		if err != nil {
+			msg := fmt.Sprintf("Invalid size: %s", rem[1])
+			s.accessLogger.Printf("%s: %s", errorPrefix, msg)
+			return "", 0, casblob.Identity,
+				status.Error(codes.InvalidArgument, msg)
+		}
+		if size < 0 {
+			msg := fmt.Sprintf("Invalid size (must be non-negative): %s", rem[1])
+			s.accessLogger.Printf("%s: %s", errorPrefix, msg)
+			return "", 0, casblob.Identity,
+				status.Error(codes.InvalidArgument, msg)
+		}
+
+		err = s.validateHash(hash, size, errorPrefix)
+		if err != nil {
+			return "", 0, casblob.Identity, err
+		}
+
+		return hash, size, casblob.Identity, nil
+	}
+
+	if !foundCompressedBlobs || len(rem) != 3 {
 		msg := fmt.Sprintf("Unable to parse resource name: %s", name)
 		s.accessLogger.Printf("%s: %s", errorPrefix, msg)
-		return "", 0, status.Error(codes.InvalidArgument, msg)
+		return "", 0, casblob.Identity,
+			status.Error(codes.InvalidArgument, msg)
+	}
+
+	if rem[0] != "zstd" {
+		msg := fmt.Sprintf("Unable to parse compressor in resource name: %s", name)
+		s.accessLogger.Printf("%s: %s", errorPrefix, msg)
+		return "", 0, casblob.Identity,
+			status.Error(codes.InvalidArgument, msg)
 	}
 
-	hash := rem[0]
+	hash := rem[1]
+	sizeStr := rem[2]
 
-	size, err := strconv.ParseInt(rem[1], 10, 64)
+	size, err := strconv.ParseInt(sizeStr, 10, 64)
 	if err != nil {
-		msg := fmt.Sprintf("Invalid size: %s", rem[1])
+		msg := fmt.Sprintf("Invalid size: %s", sizeStr)
 		s.accessLogger.Printf("%s: %s", errorPrefix, msg)
-		return "", 0, status.Error(codes.InvalidArgument, msg)
+		return "", 0, casblob.Zstandard,
+			status.Error(codes.InvalidArgument, msg)
 	}
 	if size < 0 {
 		msg := fmt.Sprintf("Invalid size (must be non-negative): %s", rem[1])
 		s.accessLogger.Printf("%s: %s", errorPrefix, msg)
-		return "", 0, status.Error(codes.InvalidArgument, msg)
+		return "", 0, casblob.Zstandard,
+			status.Error(codes.InvalidArgument, msg)
 	}
 
 	err = s.validateHash(hash, size, errorPrefix)
 	if err != nil {
-		return "", 0, err
+		return "", 0, casblob.Zstandard, err
 	}
 
-	return hash, size, nil
+	return hash, size, casblob.Zstandard, nil
 }
 
 // Seek to offset in Reader r, from the current position.
@@ -187,14 +261,14 @@ func seekReaderTo(r io.Reader, offset int64) {
 	}
 }
 
-// Parse a WriteRequest.ResourceName, return the validated hash, size and an error.
-func (s *grpcServer) parseWriteResource(r string) (string, int64, error) {
+// Parse a WriteRequest.ResourceName, return the validated hash, size,
+// compression type and an optional error.
+func (s *grpcServer) parseWriteResource(r string) (string, int64, casblob.CompressionType, error) {
 
 	// req.ResourceName is of the form:
 	// [{instance_name}/]uploads/{uuid}/blobs/{hash}/{size}[/{optionalmetadata}]
-
-	// We don't use the uuid so don't bother validating it.
-	// The path segment must exist but can be empty.
+	// Or, for compressed blobs:
+	// [{instance_name}/]uploads/{uuid}/compressed-blobs/{compressor}/{uncompressed_hash}/{uncompressed_size}[{/optional_metadata}]
 
 	fields := strings.Split(r, "/")
 	var rem []string
@@ -205,29 +279,63 @@ func (s *grpcServer) parseWriteResource(r string) (string, int64, error) {
 		}
 	}
 
-	if len(rem) < 4 || rem[1] != "blobs" {
-		return "", 0, status.Errorf(codes.InvalidArgument, "Unable to parse resource name: %s", r)
+	if len(rem) < 4 {
+		return "", 0, casblob.Identity,
+			status.Errorf(codes.InvalidArgument, "Unable to parse resource name: %s", r)
+	}
+
+	// rem[0] should hold the uuid, which we don't use- ignore it.
+
+	if rem[1] == "blobs" {
+		hash := rem[2]
+		size, err := strconv.ParseInt(rem[3], 10, 64)
+		if err != nil {
+			return "", 0, casblob.Identity,
+				status.Errorf(codes.InvalidArgument, "Unable to parse size: %s", rem[3])
+		}
+
+		if size < 0 {
+			return "", 0, casblob.Identity,
+				status.Errorf(codes.InvalidArgument, "Invalid size (must be non-negative): %d", size)
+		}
+
+		err = s.validateHash(hash, size, "GRPC BYTESTREAM READ FAILED")
+		if err != nil {
+			return "", 0, casblob.Identity, err
+		}
+
+		return hash, size, casblob.Identity, nil
+	}
+
+	if rem[1] != "compressed-blobs" || len(rem) < 5 || rem[2] != "zstd" {
+		return "", 0, casblob.Zstandard,
+			status.Errorf(codes.InvalidArgument, "Unable to parse resource name: %s", r)
 	}
 
-	hash := rem[2]
-	size, err := strconv.ParseInt(rem[3], 10, 64)
+	sizeStr := rem[4]
+
+	size, err := strconv.ParseInt(sizeStr, 10, 64)
 	if err != nil {
-		return "", 0, status.Errorf(codes.InvalidArgument, "Unable to parse size: %s", rem[3])
+		return "", 0, casblob.Zstandard,
+			status.Errorf(codes.InvalidArgument, "Unable to parse size: %s", sizeStr)
 	}
 
 	if size < 0 {
-		return "", 0, status.Errorf(codes.InvalidArgument, "Invalid size (must be non-negative): %s", rem[3])
+		return "", 0, casblob.Zstandard,
+			status.Errorf(codes.InvalidArgument, "Invalid size (must be non-negative): %d", size)
 	}
 
+	hash := rem[3]
 	err = s.validateHash(hash, size, "GRPC BYTESTREAM READ FAILED")
 	if err != nil {
-		return "", 0, err
+		return "", 0, casblob.Zstandard, err
 	}
 
-	return hash, size, nil
+	return hash, size, casblob.Zstandard, nil
 }
 
 var errWriteOffset error = errors.New("bytestream writes from non-zero offsets are unsupported")
+var errDecoderPoolFail error = errors.New("failed to get DecoderWrapper from pool")
 
 func (s *grpcServer) Write(srv bytestream.ByteStream_WriteServer) error {
 
@@ -238,6 +346,14 @@ func (s *grpcServer) Write(srv bytestream.ByteStream_WriteServer) error {
 	recvResult := make(chan error)
 	resourceNameChan := make(chan string, 1)
 
+	cmp := casblob.Identity
+	var dec *zstd.Decoder
+	defer func() {
+		if dec != nil {
+			dec.Close()
+		}
+	}()
+
 	go func() {
 		firstIteration := true
 		var resourceName string
@@ -273,7 +389,7 @@ func (s *grpcServer) Write(srv bytestream.ByteStream_WriteServer) error {
 				close(resourceNameChan)
 
 				var hash string
-				hash, size, err = s.parseWriteResource(resourceName)
+				hash, size, cmp, err = s.parseWriteResource(resourceName)
 				if err != nil {
 					s.accessLogger.Printf("GRPC BYTESTREAM WRITE FAILED: %s", err)
 					recvResult <- err
@@ -296,8 +412,25 @@ func (s *grpcServer) Write(srv bytestream.ByteStream_WriteServer) error {
 					return
 				}
 
+				var rc io.ReadCloser = pr
+				if cmp == casblob.Zstandard {
+					dec, ok := decoderPool.Get().(*syncpool.DecoderWrapper)
+					if !ok {
+						s.accessLogger.Printf("GRPC BYTESTREAM WRITE FAILED: %s", errDecoderPoolFail)
+						recvResult <- errDecoderPoolFail
+						return
+					}
+					err = dec.Reset(pr)
+					if err != nil {
+						s.accessLogger.Printf("GRPC BYTESTREAM WRITE FAILED: %s", err)
+						recvResult <- err
+						return
+					}
+					rc = dec.IOReadCloser()
+				}
+
 				go func() {
-					putResult <- s.cache.Put(cache.CAS, hash, size, pr)
+					putResult <- s.cache.Put(cache.CAS, hash, size, rc)
 				}()
 
 				firstIteration = false
@@ -317,7 +450,7 @@ func (s *grpcServer) Write(srv bytestream.ByteStream_WriteServer) error {
 			}
 			resp.CommittedSize += int64(n)
 
-			if resp.CommittedSize > size {
+			if cmp == casblob.Identity && resp.CommittedSize > size {
 				msg := fmt.Sprintf("Client sent more than %d data! %d", size, resp.CommittedSize)
 				recvResult <- status.Error(codes.OutOfRange, msg)
 				return
@@ -326,7 +459,7 @@ func (s *grpcServer) Write(srv bytestream.ByteStream_WriteServer) error {
 			// Possibly redundant check, since we explicitly check for
 			// EOF at the start of each loop.
 			if req.FinishWrite {
-				if resp.CommittedSize != size {
+				if cmp == casblob.Identity && resp.CommittedSize != size {
 					msg := fmt.Sprintf("Unexpected amount of data read: %d expected: %d",
 						resp.CommittedSize, size)
 					recvResult <- status.Error(codes.Unknown, msg)
diff --git a/server/grpc_test.go b/server/grpc_test.go
index 85acb78..8825c4c 100644
--- a/server/grpc_test.go
+++ b/server/grpc_test.go
@@ -26,7 +26,10 @@ import (
 
 	"github.com/buchgr/bazel-remote/cache"
 	"github.com/buchgr/bazel-remote/cache/disk"
+	"github.com/buchgr/bazel-remote/cache/disk/casblob"
 	"github.com/buchgr/bazel-remote/utils"
+
+	"github.com/klauspost/compress/zstd"
 )
 
 type badDigest struct {
@@ -69,7 +72,10 @@ func TestMain(m *testing.M) {
 	}
 	defer os.RemoveAll(dir)
 
-	diskCache, err = disk.New(dir, int64(10*maxChunkSize), nil)
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := int64(10 * maxChunkSize * 2)
+
+	diskCache, err = disk.New(dir, cacheSize, "zstd", nil)
 	if err != nil {
 		fmt.Println("Test setup failed")
 		os.Exit(1)
@@ -482,7 +488,7 @@ func TestGrpcAcRequestInlinedBlobs(t *testing.T) {
 		}
 
 		if r.Status.GetCode() != int32(codes.OK) {
-			t.Fatal("missing blob:", r.Digest)
+			t.Fatal("missing blob:", r.Digest, "message:", r.Status.GetMessage())
 		}
 	}
 
@@ -579,7 +585,7 @@ func TestGrpcByteStreamDeadline(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	_, sz, err := diskCache.Get(cache.CAS, testBlobHash, testBlobSize)
+	_, sz, err := diskCache.Get(cache.CAS, testBlobHash, testBlobSize, 0)
 	if err != nil {
 		t.Fatalf("get error: %v\n", err)
 	}
@@ -737,6 +743,54 @@ func TestGrpcByteStream(t *testing.T) {
 		t.Fatal("Error: bytestream read failed (data doesn't match)")
 	}
 
+	// Read again, in zstd form this time.
+
+	bsrReq = bytestream.ReadRequest{
+		ResourceName: fmt.Sprintf("%s/compressed-blobs/zstd/%s/%d",
+			instance, testBlobDigest.Hash, len(testBlob)),
+	}
+
+	bsrc, err = bsClient.Read(ctx, &bsrReq)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var decmpBuf bytes.Buffer
+	dr, dw := io.Pipe()
+	dec, err := zstd.NewReader(dr, zstd.WithDecoderConcurrency(1))
+
+	go func() {
+		for {
+			bsrResp, err = bsrc.Recv()
+			if err == io.EOF {
+				break
+			}
+			if err != nil {
+				t.Fatal(err)
+			}
+			if bsrResp == nil {
+				t.Fatalf("Expected non-nil response")
+			}
+
+			dw.Write(bsrResp.Data)
+
+			if len(downloadedBlob) > len(testBlob) {
+				t.Fatalf("Downloaded too much data")
+			}
+		}
+
+		dw.Close()
+	}()
+
+	_, err = io.Copy(&decmpBuf, dec)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !bytes.Equal(decmpBuf.Bytes(), testBlob) {
+		t.Fatal("Error: bytestream compressed read failed (data doesn't match)")
+	}
+
 	// Invalid Read's.
 
 	for _, tc := range badDigestTestCases {
@@ -773,6 +827,137 @@ func TestGrpcByteStream(t *testing.T) {
 	}
 }
 
+func TestGrpcByteStreamZstdWrite(t *testing.T) {
+	// Must be large enough to test multiple iterations of the
+	// bytestream Read Recv loop.
+	testBlobSize := int64(maxChunkSize * 3 / 2)
+	testBlob, testBlobHash := testutils.RandomDataAndHash(testBlobSize)
+	testBlobDigest := pb.Digest{
+		Hash:      testBlobHash,
+		SizeBytes: int64(len(testBlob)),
+	}
+
+	enc, err := zstd.NewWriter(nil, zstd.WithEncoderConcurrency(1))
+	if err != nil {
+		t.Fatal(err)
+	}
+	compressedBlob := enc.EncodeAll(testBlob, nil)
+	enc.Close()
+
+	bswc, err := bsClient.Write(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	instance := "ignoredInstance"
+
+	cutoff := len(compressedBlob) / 2
+	blobPart := compressedBlob[:cutoff]
+
+	bswReq := bytestream.WriteRequest{
+		ResourceName: fmt.Sprintf("%s/uploads/%s/compressed-blobs/zstd/%s/%d",
+			instance, uuid.New().String(), testBlobDigest.Hash, len(testBlob)),
+		FinishWrite: false,
+		Data:        blobPart,
+	}
+
+	err = bswc.Send(&bswReq)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	blobPart = compressedBlob[cutoff:]
+	bswReq.FinishWrite = true
+	bswReq.Data = blobPart
+
+	err = bswc.Send(&bswReq)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	bswResp, err := bswc.CloseAndRecv()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if bswResp.CommittedSize != int64(len(compressedBlob)) {
+		t.Fatalf("Error: expected to write: %d but committed: %d\n",
+			len(testBlob), bswResp.CommittedSize)
+	}
+
+	// Read back.
+
+	bsrReq := bytestream.ReadRequest{
+		ResourceName: fmt.Sprintf("%s/blobs/%s/%d",
+			instance, testBlobDigest.Hash, len(testBlob)),
+	}
+
+	bsrc, err := bsClient.Read(ctx, &bsrReq)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	downloadedBlob := make([]byte, 0, len(testBlob))
+
+	for {
+		bsrResp, err := bsrc.Recv()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			t.Fatal(err)
+		}
+		if bsrResp == nil {
+			t.Fatalf("Expected non-nil response")
+		}
+
+		downloadedBlob = append(downloadedBlob, bsrResp.Data...)
+
+		if len(downloadedBlob) > len(testBlob) {
+			t.Fatalf("Downloaded too much data")
+		}
+	}
+
+	if !bytes.Equal(downloadedBlob, testBlob) {
+		t.Fatal("Error: bytestream read failed (data doesn't match)")
+	}
+}
+
+func TestGrpcByteStreamInvalidReadLimit(t *testing.T) {
+	testBlobSize := int64(maxChunkSize)
+	testBlob, testBlobHash := testutils.RandomDataAndHash(testBlobSize)
+	testBlobDigest := pb.Digest{
+		Hash:      testBlobHash,
+		SizeBytes: int64(len(testBlob)),
+	}
+
+	// Check that non-zero ReadLimit for compressed-blobs returns
+	// InvalidArgument.
+	bsrReq := bytestream.ReadRequest{
+		ResourceName: fmt.Sprintf("ignoredinstance/compressed-blobs/zstd/%s/%d",
+			testBlobDigest.Hash, len(testBlob)),
+		ReadLimit: 1024,
+	}
+
+	bsrc, err := bsClient.Read(ctx, &bsrReq)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = bsrc.Recv()
+	if err == nil || err == io.EOF {
+		t.Fatal("Expected error due to non-zero ReadLimit for compressed-blobs read")
+	}
+
+	statusErr, ok := status.FromError(err)
+	if !ok {
+		t.Errorf("Expected a grpc status error, got something else: %v", err)
+		return
+	}
+	if statusErr.Code() != codes.InvalidArgument {
+		t.Fatal("Expected InvalidArgument response, got", err)
+	}
+}
+
 func TestGrpcByteStreamSkippedWrite(t *testing.T) {
 
 	// Must be large enough to test multiple iterations of the
@@ -1149,16 +1334,26 @@ func TestParseReadResource(t *testing.T) {
 	unusedLogPrefix := "foo"
 
 	tcs := []struct {
-		resourceName string
-		expectedHash string
-		expectedSize int64
-		expectError  bool
+		resourceName        string
+		expectedHash        string
+		expectedSize        int64
+		expectedCompression casblob.CompressionType
+		expectError         bool
 	}{
 		{
 			// No instance specified.
 			"blobs/0123456789012345678901234567890123456789012345678901234567890123/42",
 			"0123456789012345678901234567890123456789012345678901234567890123",
 			42,
+			casblob.Identity,
+			false,
+		},
+		{
+			// No instance specified.
+			"compressed-blobs/zstd/0123456789012345678901234567890123456789012345678901234567890123/42",
+			"0123456789012345678901234567890123456789012345678901234567890123",
+			42,
+			casblob.Zstandard,
 			false,
 		},
 		{
@@ -1166,6 +1361,15 @@ func TestParseReadResource(t *testing.T) {
 			"foo/blobs/0123456789012345678901234567890123456789012345678901234567890123/42",
 			"0123456789012345678901234567890123456789012345678901234567890123",
 			42,
+			casblob.Identity,
+			false,
+		},
+		{
+			// Instance specified.
+			"foo/compressed-blobs/zstd/0123456789012345678901234567890123456789012345678901234567890123/42",
+			"0123456789012345678901234567890123456789012345678901234567890123",
+			42,
+			casblob.Zstandard,
 			false,
 		},
 		{
@@ -1173,10 +1377,19 @@ func TestParseReadResource(t *testing.T) {
 			"foo/bar/blobs/0123456789012345678901234567890123456789012345678901234567890123/42",
 			"0123456789012345678901234567890123456789012345678901234567890123",
 			42,
+			casblob.Identity,
 			false,
 		},
 		{
-			// Missing "/blobs/".
+			// Instance specified, containing '/'.
+			"foo/bar/compressed-blobs/zstd/0123456789012345678901234567890123456789012345678901234567890123/42",
+			"0123456789012345678901234567890123456789012345678901234567890123",
+			42,
+			casblob.Zstandard,
+			false,
+		},
+		{
+			// Missing "/blobs/" or "/compressed-blobs/".
 			resourceName: "foo/bar/0123456789012345678901234567890123456789012345678901234567890123/42",
 			expectError:  true,
 		},
@@ -1258,10 +1471,36 @@ func TestParseReadResource(t *testing.T) {
 			resourceName: "foo/blobs//42",
 			expectError:  true,
 		},
+
+		// Unsupported/unrecognised compression types.
+		{
+			resourceName: "pretenduuid/compressed-blobs/zstandard/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "pretenduuid/compressed-blobs/Zstd/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "pretenduuid/compressed-blobs/ZSTD/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "pretenduuid/compressed-blobs/identity/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "pretenduuid/compressed-blobs/Identity/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "pretenduuid/compressed-blobs/IDENTITY/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
 	}
 
 	for _, tc := range tcs {
-		hash, size, err := s.parseReadResource(tc.resourceName, unusedLogPrefix)
+		hash, size, cmp, err := s.parseReadResource(tc.resourceName, unusedLogPrefix)
 
 		if tc.expectError {
 			if err == nil {
@@ -1282,11 +1521,16 @@ func TestParseReadResource(t *testing.T) {
 		if size != tc.expectedSize {
 			t.Fatalf("Expected size: %d did not match actual size: %d in %q", tc.expectedSize, size, tc.resourceName)
 		}
+
+		if cmp != tc.expectedCompression {
+			t.Fatalf("Expected compressor: %d did not match actual compressor: %d in %q", tc.expectedCompression, cmp, tc.resourceName)
+		}
 	}
 }
 
 func TestParseWriteResource(t *testing.T) {
 	// Format: [{instance_name}/]uploads/{uuid}/blobs/{hash}/{size}[/{optionalmetadata}]
+	// Or: [{instance_name}/]uploads/{uuid}/compressed-blobs/{compressor}/{uncompressed_hash}/{uncompressed_size}[{/optional_metadata}]
 
 	// We ignore instance_name and metadata, and we don't verify that the
 	// uuid is valid- it just needs to exist (or be empty) and not contain '/'.
@@ -1297,21 +1541,38 @@ func TestParseWriteResource(t *testing.T) {
 	}
 
 	tcs := []struct {
-		resourceName string
-		expectedHash string
-		expectedSize int64
-		expectError  bool
+		resourceName        string
+		expectedHash        string
+		expectedSize        int64
+		expectedCompression casblob.CompressionType
+		expectError         bool
 	}{
 		{
 			"foo/uploads/pretenduuid/blobs/0123456789012345678901234567890123456789012345678901234567890123/42",
 			"0123456789012345678901234567890123456789012345678901234567890123",
 			42,
+			casblob.Identity,
+			false,
+		},
+		{
+			"foo/uploads/pretenduuid/compressed-blobs/zstd/0123456789012345678901234567890123456789012345678901234567890123/42",
+			"0123456789012345678901234567890123456789012345678901234567890123",
+			42,
+			casblob.Zstandard,
 			false,
 		},
 		{
 			"uploads/pretenduuid/blobs/0123456789012345678901234567890123456789012345678901234567890123/42",
 			"0123456789012345678901234567890123456789012345678901234567890123",
 			42,
+			casblob.Identity,
+			false,
+		},
+		{
+			"uploads/pretenduuid/compressed-blobs/zstd/0123456789012345678901234567890123456789012345678901234567890123/42",
+			"0123456789012345678901234567890123456789012345678901234567890123",
+			42,
+			casblob.Zstandard,
 			false,
 		},
 		{
@@ -1319,12 +1580,29 @@ func TestParseWriteResource(t *testing.T) {
 			"uploads/pretenduuid/blobs/0123456789012345678901234567890123456789012345678901234567890123/9223372036854775807",
 			"0123456789012345678901234567890123456789012345678901234567890123",
 			9223372036854775807,
+			casblob.Identity,
+			false,
+		},
+		{
+			// max(int64)
+			"uploads/pretenduuid/compressed-blobs/zstd/0123456789012345678901234567890123456789012345678901234567890123/9223372036854775807",
+			"0123456789012345678901234567890123456789012345678901234567890123",
+			9223372036854775807,
+			casblob.Zstandard,
 			false,
 		},
 		{
 			"foo/uploads/pretenduuid/blobs/0123456789012345678901234567890123456789012345678901234567890123/42/some/meta/data",
 			"0123456789012345678901234567890123456789012345678901234567890123",
 			42,
+			casblob.Identity,
+			false,
+		},
+		{
+			"foo/uploads/pretenduuid/compressed-blobs/zstd/0123456789012345678901234567890123456789012345678901234567890123/42/some/meta/data",
+			"0123456789012345678901234567890123456789012345678901234567890123",
+			42,
+			casblob.Zstandard,
 			false,
 		},
 
@@ -1393,10 +1671,36 @@ func TestParseWriteResource(t *testing.T) {
 			resourceName: "foo/blobs/0123456789012345678901234567890123456789012345678901234567890123/9223372036854775808",
 			expectError:  true,
 		},
+
+		// Unsupported/unrecognised compression types.
+		{
+			resourceName: "uploads/pretenduuid/compressed-blobs/zstandard/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "uploads/pretenduuid/compressed-blobs/Zstd/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "uploads/pretenduuid/compressed-blobs/ZSTD/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "uploads/pretenduuid/compressed-blobs/identity/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "uploads/pretenduuid/compressed-blobs/Identity/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
+		{
+			resourceName: "uploads/pretenduuid/compressed-blobs/IDENTITY/0123456789012345678901234567890123456789012345678901234567890123/42",
+			expectError:  true,
+		},
 	}
 
 	for _, tc := range tcs {
-		hash, size, err := s.parseWriteResource(tc.resourceName)
+		hash, size, cmp, err := s.parseWriteResource(tc.resourceName)
 
 		if tc.expectError {
 			if err == nil {
@@ -1417,5 +1721,9 @@ func TestParseWriteResource(t *testing.T) {
 		if size != tc.expectedSize {
 			t.Fatalf("Expected size: %d did not match actual size: %d in %q", tc.expectedSize, size, tc.resourceName)
 		}
+
+		if cmp != tc.expectedCompression {
+			t.Fatalf("Expected compression: %d did not match actual compression: %d in %q", tc.expectedCompression, cmp, tc.resourceName)
+		}
 	}
 }
diff --git a/server/http_test.go b/server/http_test.go
index 64db420..85a47b3 100644
--- a/server/http_test.go
+++ b/server/http_test.go
@@ -12,7 +12,6 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"os"
-	"path/filepath"
 	"sync"
 	"testing"
 
@@ -30,27 +29,28 @@ func TestDownloadFile(t *testing.T) {
 
 	blobSize := int64(1024)
 
-	hash, err := testutils.CreateCacheFile(filepath.Join(cacheDir, "cas"), blobSize)
-	if err != nil {
-		t.Fatal(err)
-	}
+	data, hash := testutils.RandomDataAndHash(blobSize)
 
-	c, err := disk.New(cacheDir, blobSize, nil)
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := blobSize * 2
+
+	c, err := disk.New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
 	h := NewHTTPCache(c, testutils.NewSilentLogger(), testutils.NewSilentLogger(), true, false, "")
 
-	req, err := http.NewRequest("GET", "/cas/"+hash, bytes.NewReader([]byte{}))
-	if err != nil {
-		t.Fatal(err)
-	}
 	rr := httptest.NewRecorder()
 	handler := http.HandlerFunc(h.CacheHandler)
-	handler.ServeHTTP(rr, req)
+
+	pr := httptest.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(data))
+	handler.ServeHTTP(rr, pr)
+
+	gr := httptest.NewRequest("GET", "/cas/"+hash, nil)
+	handler.ServeHTTP(rr, gr)
 
 	if status := rr.Code; status != http.StatusOK {
-		t.Fatal("Handler returned wrong status code",
+		t.Fatal("Handler returned wrong status code for", hash,
 			"expected", http.StatusOK,
 			"got", status,
 		)
@@ -72,11 +72,8 @@ func TestDownloadFile(t *testing.T) {
 		)
 	}
 
-	req, err = http.NewRequest("HEAD", "/cas/"+hash, bytes.NewReader([]byte{}))
-	if err != nil {
-		t.Fatal(err)
-	}
-	handler.ServeHTTP(rr, req)
+	hr := httptest.NewRequest("HEAD", "/cas/"+hash, nil)
+	handler.ServeHTTP(rr, hr)
 	rsp = rr.Result()
 	if contentLen := rsp.ContentLength; contentLen != blobSize {
 		t.Error("HEAD request returned wrong content length",
@@ -90,18 +87,19 @@ func TestUploadFilesConcurrently(t *testing.T) {
 	defer os.RemoveAll(cacheDir)
 
 	const NumUploads = 1000
+	const blobSize = 1024
 
 	var requests [NumUploads]*http.Request
 	for i := 0; i < NumUploads; i++ {
-		data, hash := testutils.RandomDataAndHash(1024)
-		r, err := http.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(data))
-		if err != nil {
-			t.Fatal(err)
-		}
+		data, hash := testutils.RandomDataAndHash(blobSize)
+		r := httptest.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(data))
 		requests[i] = r
 	}
 
-	c, err := disk.New(cacheDir, 1000*1024, nil)
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := int64(NumUploads * blobSize * 2)
+
+	c, err := disk.New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -162,7 +160,10 @@ func TestUploadSameFileConcurrently(t *testing.T) {
 
 	numWorkers := 100
 
-	c, err := disk.New(cacheDir, int64(len(data)*numWorkers), nil)
+	// Add some overhead for likely CAS blob storage expansion.
+	cacheSize := int64(len(data) * numWorkers * 2)
+
+	c, err := disk.New(cacheDir, cacheSize, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -175,14 +176,9 @@ func TestUploadSameFileConcurrently(t *testing.T) {
 		go func() {
 			defer wg.Done()
 
-			rr := httptest.NewRecorder()
-
-			request, err := http.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(data))
-			if err != nil {
-				t.Error(err)
-				return
-			}
+			request := httptest.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(data))
 
+			rr := httptest.NewRecorder()
 			handler.ServeHTTP(rr, request)
 
 			if status := rr.Code; status != http.StatusOK {
@@ -206,12 +202,9 @@ func TestUploadCorruptedFile(t *testing.T) {
 	data, hash := testutils.RandomDataAndHash(1024)
 	corruptedData := data[:999]
 
-	r, err := http.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(corruptedData))
-	if err != nil {
-		t.Fatal(err)
-	}
+	r := httptest.NewRequest("PUT", "/cas/"+hash, bytes.NewReader(corruptedData))
 
-	c, err := disk.New(cacheDir, 2048, nil)
+	c, err := disk.New(cacheDir, 2048, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -249,12 +242,9 @@ func TestUploadEmptyActionResult(t *testing.T) {
 
 	data, hash := testutils.RandomDataAndHash(0)
 
-	r, err := http.NewRequest("PUT", "/ac/"+hash, bytes.NewReader(data))
-	if err != nil {
-		t.Fatal(err)
-	}
+	r := httptest.NewRequest("PUT", "/ac/"+hash, bytes.NewReader(data))
 
-	c, err := disk.New(cacheDir, 2048, nil)
+	c, err := disk.New(cacheDir, 2048, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -271,8 +261,11 @@ func TestUploadEmptyActionResult(t *testing.T) {
 			"got", status)
 	}
 
-	cacheFile := filepath.Join(cacheDir, "ac", hash[:2], hash)
-	cachedData, err := ioutil.ReadFile(cacheFile)
+	getReq := httptest.NewRequest("GET", "/ac/"+hash, nil)
+	rr2 := httptest.NewRecorder()
+	handler.ServeHTTP(rr2, getReq)
+
+	cachedData, err := ioutil.ReadAll(rr2.Result().Body)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -309,12 +302,9 @@ func testEmptyBlobAvailable(t *testing.T, method string) {
 	defer os.RemoveAll(cacheDir)
 
 	data, hash := testutils.RandomDataAndHash(0)
-	r, err := http.NewRequest(method, "/cas/"+hash, bytes.NewReader(data))
-	if err != nil {
-		t.Fatal(err)
-	}
+	r := httptest.NewRequest(method, "/cas/"+hash, bytes.NewReader(data))
 
-	c, err := disk.New(cacheDir, 2048, nil)
+	c, err := disk.New(cacheDir, 2048, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -337,12 +327,9 @@ func TestStatusPage(t *testing.T) {
 	cacheDir := testutils.TempDir(t)
 	defer os.RemoveAll(cacheDir)
 
-	r, err := http.NewRequest("GET", "/status", bytes.NewReader([]byte{}))
-	if err != nil {
-		t.Fatal(err)
-	}
+	r := httptest.NewRequest("GET", "/status", nil)
 
-	c, err := disk.New(cacheDir, 2048, nil)
+	c, err := disk.New(cacheDir, 2048, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -485,7 +472,7 @@ func TestRemoteReturnsNotFound(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer os.RemoveAll(cacheDir)
-	emptyCache, err := disk.New(cacheDir, 1024, nil)
+	emptyCache, err := disk.New(cacheDir, 1024, "zstd", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/utils/tempfile/tempfile_test.go b/utils/tempfile/tempfile_test.go
index c6ea40d..e758500 100644
--- a/utils/tempfile/tempfile_test.go
+++ b/utils/tempfile/tempfile_test.go
@@ -19,14 +19,24 @@ func TestTempfileCreator(t *testing.T) {
 	}
 	defer os.RemoveAll(dir)
 
-	targetFile := path.Join(dir, "foo")
-	tf, err := tfc.Create(targetFile)
+	targetFileBase := path.Join(dir, "foo")
+	tf, random, err := tfc.Create(targetFileBase, false)
 	if err != nil {
 		t.Fatal(err)
 	}
 	defer os.Remove(tf.Name())
 
-	expectedPrefix := targetFile + "."
+	if random == "" {
+		t.Fatalf("Expected non-empty random string in the filename: %q",
+			tf.Name())
+	}
+
+	if !strings.Contains(tf.Name(), random) {
+		t.Fatalf("Expected filename %q to contain random string %q",
+			tf.Name(), random)
+	}
+
+	expectedPrefix := targetFileBase
 	if !strings.HasPrefix(tf.Name(), expectedPrefix) {
 		t.Fatalf("Expected tempfile \"%s\" to have prefix \"%s\"",
 			tf.Name(), expectedPrefix)
diff --git a/utils/testutils.go b/utils/testutils.go
index f0ee560..2a65136 100644
--- a/utils/testutils.go
+++ b/utils/testutils.go
@@ -6,7 +6,6 @@ import (
 	"encoding/hex"
 	"io/ioutil"
 	"log"
-	"os"
 	"testing"
 )
 
@@ -20,18 +19,6 @@ func TempDir(t *testing.T) string {
 	return dir
 }
 
-// CreateCacheFile creates a random data file of the given size in the
-// provided directory (corresponding to one of bazel-remote's keyspaces)
-// and returns its sha256 hash and any error that occurred.
-func CreateCacheFile(dir string, size int64) (string, error) {
-	data, hash := RandomDataAndHash(size)
-	subdir := dir + "/" + hash[0:2]
-	os.MkdirAll(subdir, os.ModePerm)
-	filepath := subdir + "/" + hash
-
-	return hash, ioutil.WriteFile(filepath, data, os.ModePerm)
-}
-
 // RandomDataAndHash creates a random blob of the specified size, and
 // returns that blob along with its sha256 hash.
 func RandomDataAndHash(size int64) ([]byte, string) {