Skip to main content

Cryptostore

License Python Container PyPI

Cryptostore subscribes to cryptocurrency exchanges with cryptofeed, stores the normalized data in your database and streams it in real time to your own applications. One config file powers the whole collection. You can run cryptostore as a container, or as a Python package.

backends:
  postgres:
    type: postgres
    host: db
    database: cryptostore
    user: cryptostore
    password: ${PGPASSWORD}
  stream:
    type: stream            # cryptostore listens, your programs connect
    tcp: 0.0.0.0:8080
    zmq: tcp://0.0.0.0:5555

exchanges:
  COINBASE:
    channels:
      trades: [BTC-USD, ETH-USD]
      l2_book: {symbols: [BTC-USD], backends: [stream]}    # books only to the stream
    options:
      max_depth: 25
  BINANCE_FUTURES:
    channels:
      trades: all                        # every symbol the exchange lists
      liquidations: ['*-USDT-PERP']      # or a pattern
      funding: [BTC-USDT-PERP]

workers: 4                               # threads
  • Exchanges and channels: everything cryptofeed supports - 30 exchanges, trades, l2_book, l3_book, l1_book, ticker, funding, open_interest, liquidations, index, candles
  • Storage backends: PostgreSQL, Redis (sorted sets or streams), MongoDB, InfluxDB 2, QuestDB, Kafka
  • Streaming: a built-in server that fans every update out over TCP, unix sockets and ZeroMQ PUB, plus cryptofeed's client-mode socket and ZeroMQ outputs
  • Routing: per exchange or per channel, any number of backends at once
  • Symbols: explicit lists, all, or glob patterns expanded from the exchange's symbol list
  • Scaling: large subscriptions are sharded across worker threads, each with its own event loop. On free-threaded Python (3.15t) they run on separate cores
  • Raw capture: optionally record every exchange message to pcap files cryptofeed can replay
  • Operations: strict config validation with suggestions, stats reports with an event-loop lag metric, a health file for container health checks, graceful shutdown on SIGTERM

Running with Docker

docker pull ghcr.io/bmoscon/cryptostore:latest
docker run --rm -v "$PWD/config.yaml:/config/config.yaml:ro" -p 8080:8080 ghcr.io/bmoscon/cryptostore:latest

The container reads /config/config.yaml (override with CRYPTOSTORE_CONFIG or pass the path after run), writes to /data, and runs as an unprivileged user. Two variants are published:

tag interpreter
latest, 2.0.0 CPython 3.15
latest-t, 2.0.0-t CPython 3.15t, the free-threaded build - use this one with workers

Build locally with docker build -t cryptostore . or docker build --build-arg PYTHON_VERSION=3.15t -t cryptostore:t ..

examples/compose has ready to run docker compose set-ups: Postgres plus streaming, and streaming only.

Running without Docker

Python 3.15 or 3.15t is required.

uv tool install 'cryptostore[all]'      # or: pip install 'cryptostore[all]'
cryptostore run config.yaml

The all extra installs every backend's driver. Pick individual ones with cryptostore[postgres], [redis], [mongo], [kafka], [zmq] (the stream backend's TCP and unix endpoints need nothing extra).

Free-threaded Python. Every dependency keeps the GIL disabled on 3.15t. cryptostore logs a warning at start-up when it finds the GIL enabled on a free-threaded build, and the health file reports gil_enabled.

Commands

command purpose
cryptostore run [config] run the collector
cryptostore check [config] validate a config and print the worker plan without connecting to any backend or feed. Symbol patterns are expanded, which downloads those exchanges' symbol lists. --symbols also verifies every explicit symbol exists
cryptostore exchanges [name] list exchanges and their channels, or one exchange with the options it accepts
cryptostore backends list backend types and the channels each can store
cryptostore health [config] exit 0 if the running collector's health file is fresh - the container HEALTHCHECK

Configuration

YAML or JSON is accepted. Cryptostore looks at the path on the command line, $CRYPTOSTORE_CONFIG, or ./config.yaml. The container sets CRYPTOSTORE_CONFIG=/config/config.yaml, so mounting a file there is enough. examples/config.yaml shows every option with its default.

Any string value may contain ${NAME} or ${NAME:-default}. A reference to an unset variable with no default is an error. When the whole value is one reference the result is parsed as YAML, so port: ${PGPORT} becomes an integer.

Validation is strict and runs before anything connects: unknown keys, exchanges, channels, backends and feed options will cause errors.

backends

A mapping of backend name to definition. They map to crytofeed backends except these three:

key purpose
book extra arguments for the l2_book/l3_book writer: snapshot_interval (full book every N deltas, default 1000) and snapshots_only (never send deltas). The questdb backend takes depth instead
queue cryptofeed BackendQueue tuning: max_depth, overflow (drop_oldest, drop_new, block), batch_max, batch_interval, flush_deadline, retry: {max_attempts, base, jitter, max_delay}
numeric_type decimal, float or str - how prices and sizes are written. Each type has cryptofeed's default when omitted
type what it does options (aliases in parentheses)
stream cryptostore's streaming server. Binds endpoints that your applications connect to. See Streaming tcp (host:port), uds (socket path), zmq (PUB endpoint), client_queue, zmq_hwm
postgres PostgreSQL, one table per data type. Tables must exist, see PostgreSQL tables host, port, db (database), user, pw (password), table, custom_columns
redis Redis sorted sets, one per exchange and symbol, scored by timestamp host, port, socket, key, score_key
redis_stream Redis Streams (XADD), one stream per exchange and symbol host, port, socket, key
mongo MongoDB, one collection per data type db (database), host, port, uri, key, client_options
influxdb InfluxDB 2.x write API addr (host, url), org, bucket, token, key
questdb QuestDB line protocol over HTTP. Books are stored as the top depth levels host, port, key
kafka Kafka. Every option other than key/numeric_type/none_to goes to AIOKafkaProducer bootstrap_servers, client_id, acks, ...
socket cryptofeed's client-mode socket backend: connects to a listener you run addr (host, url) as tcp://, udp:// or uds://, port, mtu
zmq cryptofeed's client-mode ZeroMQ PUB: connects to a SUB you bind host, port, key, dynamic_key
tty prints every update to stdout, for debugging none

Every type takes every channel except Postgres, which has no table layout for l1_book. Each worker creates one writer per backend and channel, shared by every exchange that worker handles, so twenty exchanges writing trades to one Postgres backend share one connection pool per worker.

exchanges

A mapping of cryptofeed exchange id (case-insensitive) to what to subscribe to. cryptostore exchanges lists them. cryptostore exchanges binance shows one with the options it accepts.

exchanges:
  COINBASE:
    backends: [postgres, stream]   # default for this exchange's channels (default: every backend)
    channels:
      trades: [BTC-USD, ETH-USD]   # a list of symbols routes to the exchange's default backends
      l2_book:                     # or a mapping to route one channel elsewhere
        symbols: all
        backends: [stream]
    options:                       # keyword arguments for the cryptofeed Feed
      max_depth: 25
      retries: -1
    shards: 4                      # at most this many shards when split across workers (default: no limit)

Symbols follow cryptofeed's normalized naming (BTC-USD, BTC-USDT-PERP, BTC-USD-26H05). A symbol list may also contain all or glob patterns (*-USDT, BTC-*, *-USDT-PERP), which are expanded from the exchange's symbol list when cryptostore starts. A pattern that matches nothing is an error. Explicit symbols are checked when the feed starts, or ahead of time with cryptostore check --symbols.

workers

workers:
  count: 4            # worker threads, or auto for one per CPU. Default 1
  min_symbols: 50     # split an exchange only into shards of at least this many symbols. Default 50
  lag_warn: 1.0       # seconds of event-loop lag that log a warning. Default 1

workers: 4 is shorthand for count: 4. Each worker is a thread with its own event loop running its own cryptofeed FeedHandler. An exchange with enough symbols is split into shards (every n-th symbol of every channel), shards are dealt to workers largest first, and workers that would get nothing are dropped. cryptostore check prints the plan. See Scaling for why and when.

record needs count: 1: cryptofeed's recorder is bound to one event loop.

Everything else

key default purpose
record off record the raw exchange traffic to pcap files cryptofeed can replay. A path string, or {path, rotate_size, compress}. A directory gets timestamped files
log {level: INFO} level and an optional rotating file (stdout is always used)
stats_interval 60 seconds between stats reports. 0 disables them and the health file
health_file off a JSON file rewritten with every stats report, see Stats and health. cryptostore health exits 0 while it is fresh, which is what the container HEALTHCHECK runs
on_feed_error raise raise: a permanently failed feed stops cryptostore (exit non-zero, let the container restart). remove_feed: drop it and keep the rest running
cryptofeed {} passed to cryptofeed's Config: uvloop, symbol_cache_ttl, ignore_invalid_instruments, and per-exchange credentials. log is managed by cryptostore and not allowed here

Streaming

The stream backend turns cryptostore into a market data server: cryptostore binds the endpoints and your applications connect. Every update routed to the backend goes to every connected client, filter on the client side, or with a ZeroMQ subscription prefix.

endpoint option wire format
TCP tcp: 0.0.0.0:8080 newline-delimited JSON frames {"type": "<channel>", "data": {...}}
Unix socket uds: /data/cryptostore.sock the same frames
ZeroMQ PUB zmq: tcp://0.0.0.0:5555 <EXCHANGE>-<channel>-<SYMBOL> <json>, so a SUB socket can subscribe by prefix

data is the update as cryptofeed seriaizes it plus receipt_timestamp. Prices and sizes are exact decimals on the wire (numeric_type: decimal, the default), so parse them as decimals. A book frame carries either book (a full snapshot: {"bid": {price: size}, "ask": {...}}) or delta (changed levels as [price, size] pairs, size 0 means removed). A full book is sent first, then deltas, then another full book every snapshot_interval deltas so late joiners can resynchronize. snapshots_only sends only full books. The TCP and unix frames are byte for byte what cryptofeed's own socket backend writes, and the ZeroMQ messages match cryptofeed's ZeroMQ backend, so readers written for either work.

Each TCP and unix client has its own queue of client_queue frames (default 10000). A client that falls further behind is disconnected rather than silently fed a gap: a client building a book from deltas needs to know it missed data, and reconnecting gets it a fresh snapshot. ZeroMQ applies its own high-water mark (zmq_hwm) and drops for slow subscribers, as PUB sockets do.

With several workers, frames from different shards interleave, but a symbol lives in exactly one shard so every client sees each symbol's updates in order.

examples/clients/tcp_client.py and examples/clients/zmq_client.py are complete readers. From another language, read lines from the socket and parse each as JSON.

Scaling

cryptostore is one process. Without workers everything runs on one event loop, and one loop is one core: parsing, order book maintenance and serialisation for every subscription share it. That is plenty for a few hundred symbols. It is not enough for every book on a large exchange.

When a loop cannot keep up, nothing is dropped at first. Each websocket connection buffers up to 1024 frames, then stops reading, so TCP backpressure builds and latency grows. After about a minute the keepalive timeout closes the connection, cryptofeed reconnects, and for book channels every symbol's snapshot is fetched again under the exchange's REST rate limits. The stats report shows this as climbing loop_lag, then rising reconnects.

workers is the fix. Set count to the cores you can spend and run the free-threaded image (latest-t): the worker threads then run in parallel, each with its own share of the shards, while the main loop only fans frames out to stream clients and writes the stats. On the standard image the same config still runs correctly, but the threads take turns on one core, so there prefer several containers with the config split between them. Each worker has its own database writers, so a Postgres backend with four workers holds four connection pools.

Every exchange still limits connections and REST weight per IP. shards caps how many connections one exchange is split into, cryptofeed's delay_start option staggers reconnects, and splitting across containers on different hosts splits the IP limits too.

Stats and health

Every stats_interval seconds cryptostore logs, and writes to health_file as JSON:

field content
timestamp, version, python, free_threaded, gil_enabled when, and on what
feeds, removed_feeds running feeds by shard label (BINANCE[2/4]), and feeds dropped by on_feed_error: remove_feed
workers per worker: feeds, symbols, alive, loop_lag (seconds the loop is currently behind) and loop_lag_max (worst since the previous report)
main_loop the same lag fields for the main loop (stream fan-out)
backends per backend and channel, summed over the workers' writers: written, qsize, batches, dropped_overflow, dropped_failed, retries, last_error, last_write_ts. Stream backends add clients, connections, published, kicked
connections per exchange websocket: received, reconnects, watchdog_trips, open

A worker whose loop_lag_max exceeds workers.lag_warn logs a warning. cryptostore health checks that the file is fresh and lists feeds. docker ps shows the result.

PostgreSQL tables

The postgres backend inserts into tables that must already exist, one per data type. examples/compose/postgres/init.sql creates all of them and is what the compose example mounts into Postgres. cryptofeed's classes print their own CREATE TABLE:

python -c "from cryptofeed.backends import postgres as pg; print(pg.TradePostgres.create_table_sql())"

table renames the table a backend writes to, custom_columns maps update fields to your own columns.

Development

uv sync --all-extras
uv run ruff check .
uv run pytest
make test-ft        # the same tests on the free-threaded interpreter

The tests need no network and no databases. CI runs lint and the tests on 3.15 and 3.15t, builds the container for both interpreters on amd64 and arm64, and publishes the release images and the PyPI package when a GitHub release is published.

Releasing

Bump version in pyproject.toml, add the entry to CHANGES.md, commit, then create a GitHub release whose tag is v<version> (the release page can create the tag). Publishing the release triggers two workflows:

  • pypi.yml builds the wheel and sdist, checks that the version matches the tag, and uploads to PyPI through trusted publishing - no API token is stored.
  • docker.yml builds both images for amd64 and arm64 and pushes <version>, <major.minor>, latest and the -t variants to ghcr.io/bmoscon/cryptostore.

One-time setup, both in the browser: on pypi.org open the cryptostore project, Publishing, and add a GitHub publisher with owner bmoscon, repository cryptostore, workflow pypi.yml, environment pypi, then create an environment named pypi under the repository's Settings, Environments. On GitHub open the cryptostore package under Packages, Package settings, Manage Actions access, and give this repository the write role - needed because the package predates these workflows.

Future Work

More features are in the works! If you'd like to discuss ongoing development, please join the discord.

Contributing

Issues and PRs are welcomed!

For questions about the data itself (symbols, channels, exchanges, etc) see cryptofeed.

Donations / Support

Support and donations are appreciated but not required. You can donate via GitHub Sponsors, or via the addresses below:

  • Bitcoin: bc1qm0kxz8hqacaglku5fjhfe9a5hjnuyfwk02lsyr
  • Ethereum: 0x690709FEe13eEce9E7852089BB2D53Ae5D073154

Metadata

Release files for cryptostore 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cryptostore 2.0.0
File Size Uploaded Provenance Actions
cryptostore-2.0.0.tar.gz 63.7 kB With provenance Details for cryptostore-2.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cryptostore 2.0.0
File Interpreter ABI Platform Actions
cryptostore-2.0.0-py3-none-any.whl Python 3 none any Details for cryptostore-2.0.0-py3-none-any.whl

Total release size: 112.0 kB

Release files / cryptostore-2.0.0.tar.gz

Download URL cryptostore-2.0.0.tar.gz
Size 63.7 kB
Tags Source
SHA-256 checksum
How to use checksums
172b70a3957c86c21b0a3b1d9bf1d556c83a7fdc1a84894c3f0ba495bd7d88db
BLAKE2b-256 checksum
How to use checksums
dc80a64ccd666099a51fc2a86242064bd8e7af8272e3908d9a6b6b9096da4810
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / cryptostore-2.0.0-py3-none-any.whl

Download URL cryptostore-2.0.0-py3-none-any.whl
Size 48.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6bab786dc598dd02d7c1512b1ff237dcb74f5ef2c8750fea23376682260339d4
BLAKE2b-256 checksum
How to use checksums
8ae8040ba8937e58565cccea09c8f4557877d97923fc7242fbb6e76c975a990c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Security for cryptostore 2.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cryptostore-2.0.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page