Skip to content

Connection Lifecycle: From TCP Handshake to Steady-State Mining

When an ASIC miner powers on and connects to a mining pool, it goes through a precise sequence of steps before it starts earning you money. Think of it like starting a new job: first you show up at the building (TCP connect), then you check in at the front desk (subscribe), show your employee badge (authorize), get told how hard your tasks should be (set_difficulty), receive your first assignment (notify), and then you start working (hashing and submitting).

This article walks through every step in that sequence, with real JSON-RPC messages you would see on the wire.

Here is the full lifecycle, from cold start to steady-state mining:

MINER POOL
| |
| Step 1: TCP Connect |
|-------- SYN --------------------------------->|
|<------- SYN-ACK -----------------------------|
|-------- ACK --------------------------------->|
| |
| Step 2 (optional): mining.configure |
| (version rolling negotiation) |
|-------- mining.configure -------------------->|
|<------- configure result ---------------------|
| |
| Step 3: mining.subscribe |
| (register for notifications, get extraNonce) |
|-------- mining.subscribe -------------------->|
|<------- subscribe result ---------------------|
| |
| Step 4: mining.authorize |
| (authenticate the worker) |
|-------- mining.authorize -------------------->|
|<------- authorize result ---------------------|
| |
| Step 5: Pool sends initial parameters |
|<------- mining.set_difficulty ----------------|
|<------- mining.notify (first job) ------------|
| |
| Step 6: Steady-state mining loop |
| [miner hashes... finds valid share] |
|-------- mining.submit ----------------------->|
|<------- submit result ------------------------|
| |
| [pool finds new block or adjusts difficulty] |
|<------- mining.notify (new job) --------------|
|<------- mining.set_difficulty (adjustment) ---|
| |
| [cycle continues indefinitely] |
| |

Let us walk through each step in detail.

Before any Stratum messages are exchanged, the miner needs to establish a TCP connection to the pool. This is standard networking — a three-way handshake (SYN, SYN-ACK, ACK) that creates a reliable, bidirectional communication channel.

The miner connects to a specific host and port provided by the pool. For example:

  • stratum+tcp://pool.example.com:3333 — standard unencrypted
  • stratum+ssl://pool.example.com:3334 — TLS-encrypted (some pools support this)

The port number matters. Many pools run different ports for different difficulty levels or features:

PortCommon usage
3333Standard difficulty (ASICs)
3334High difficulty
3335NiceHash or proxy connections
25Low difficulty (older/smaller devices)

Once the TCP connection is established, the miner and pool can begin exchanging JSON-RPC messages. Every message is a single line of JSON terminated by \n.

Modern ASICs that support version rolling (a technique related to AsicBoost) will send a mining.configure message before subscribing. This negotiates which protocol extensions the miner and pool both support.

Miner sends:

{
"id": 1,
"method": "mining.configure",
"params": [
["version-rolling"],
{"version-rolling.mask": "1fffe000", "version-rolling.min-bit-count": 2}
]
}

Pool responds:

{
"id": 1,
"result": {
"version-rolling": true,
"version-rolling.mask": "1fffe000"
},
"error": null
}

The version-rolling.mask tells the miner which bits of the block version field it is allowed to modify. This gives the ASIC extra nonce space to search without touching the actual nonce or extraNonce fields. We will cover this in more detail in the article on mining.subscribe.

This is where the miner registers itself with the pool. Think of it as checking into a hotel — you get a room number (session ID) and a key (extraNonce1).

Miner sends:

{
"id": 2,
"method": "mining.subscribe",
"params": ["bmminer/2.0.0", null]
}

The parameters are:

  1. User agent string — identifies the mining software and version
  2. Previous subscription IDnull for a fresh connection, or a session ID if trying to resume

Pool responds:

{
"id": 2,
"result": [
[
["mining.set_difficulty", "deadbeef00000000"],
["mining.notify", "deadbeef00000001"]
],
"a]f1c230",
4
],
"error": null
}

The response contains three critical pieces of information:

  1. Subscription pairs — tells the miner which notifications it is subscribed to (set_difficulty and notify), each with a subscription ID
  2. extraNonce1 — a hex string ("a]f1c230") that uniquely identifies this miner’s session. Every share this miner submits will include this value.
  3. extraNonce2 size — the number of bytes (4 in this example) the miner can use for its own nonce iteration

The extraNonce1 is assigned by the pool and must not be changed by the miner. The pool uses it to tell shares apart from different miners.

Now the miner identifies itself — who is this worker, and which account should the mining rewards go to?

Miner sends:

{
"id": 3,
"method": "mining.authorize",
"params": ["username.worker1", "x"]
}

The parameters are:

  1. Worker name — typically in the format account.worker or wallet_address.worker
  2. Password — usually ignored by most pools (often just "x" or ""), though some pools use it for configuration

Pool responds (success):

{
"id": 3,
"result": true,
"error": null
}

Pool responds (failure):

{
"id": 3,
"result": null,
"error": [24, "Unauthorized worker", null]
}

If authorization fails, the miner cannot submit shares. Most implementations will close the connection and retry with correct credentials.

Once the miner is subscribed and authorized, the pool immediately sends two critical pieces of information:

{
"id": null,
"method": "mining.set_difficulty",
"params": [512]
}

This tells the miner the current share difficulty. The miner must find hashes that are below the target corresponding to this difficulty value. Note that id is null — this is a notification, not a request.

{
"id": null,
"method": "mining.notify",
"params": [
"bf",
"0000000000000000000354da8e026f48a0467f9d5290a7e30b86e1a80213e5fe",
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4b03a8960cfabe6d6d",
"0100000001000000001976a914cb72e7463c3544121c5f37a8f3296c16c6a1289388ac00000000",
[
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35"
],
"20000000",
"1705ae3a",
"64a5f8c2",
true
]
}

This is the actual work the miner needs to do. We will dissect every field of mining.notify in a later article, but for now, the key thing is: this message contains everything the miner needs to construct block headers and start hashing.

The last parameter (true) is the clean_jobs flag. When it is true, it means “throw away all your previous work and switch to this immediately.” For the first job, it is always true.

Now the miner is fully operational. It enters a loop that continues for as long as the connection is alive:

  1. Hash — The miner constructs block headers using the data from mining.notify and iterates through the nonce space, looking for hashes that meet the difficulty target.

  2. Submit shares — When the miner finds a hash below the target, it sends a mining.submit:

{
"id": 4,
"method": "mining.submit",
"params": [
"username.worker1",
"bf",
"00000001",
"64a5f8c2",
"1a2b3c4d"
]
}

The pool responds with success or an error:

{"id": 4, "result": true, "error": null}
  1. Receive new jobs — The pool periodically sends new mining.notify messages. These can come for several reasons:

    • A new block was found on the network (clean_jobs = true)
    • The pool wants to update the transaction set in the block template (clean_jobs = false)
    • Enough time has passed that the timestamp should be refreshed
  2. Difficulty adjustments — The pool monitors the miner’s share submission rate and adjusts difficulty via mining.set_difficulty. If you are submitting shares too quickly, difficulty goes up. Too slowly, and it goes down.

This loop continues indefinitely — hours, days, weeks — until something breaks the connection.

Connections drop. It is a fact of life. Power blips, router reboots, pool maintenance, ISP hiccups — there are countless reasons a TCP connection might die. Here is what a well-behaved miner does:

The miner detects the drop in one of several ways:

  • TCP reset or FIN received from the pool
  • Write to socket fails (broken pipe)
  • No data received for too long (the miner’s own keepalive timeout)

Some miners send periodic mining.ping messages (or any dummy request) to detect dead connections. If the pool does not respond within a timeout, the connection is considered dead.

Connection drops
|
v
Wait (backoff period)
|
v
Try primary pool -----> Success? ---> Re-subscribe + Re-authorize
| |
| Fail v
v Resume mining with new
Try backup pool extraNonce1 and new jobs
|
| Fail
v
Increase backoff
(exponential: 1s, 2s, 4s, 8s... up to 60s)
|
v
Try primary pool again
[loop]

Key points about reconnection:

  1. Re-subscribe is mandatory. After reconnecting, the miner must go through mining.subscribe and mining.authorize again. The pool may assign a different extraNonce1.

  2. Old work is invalid. Any shares the miner was working on from the previous session are worthless. The miner must wait for new mining.notify messages.

  3. Exponential backoff. A good miner does not hammer the pool with reconnection attempts. It waits 1 second, then 2, then 4, then 8, and so on, capping at around 60 seconds. This prevents overwhelming the pool during an outage.

  4. Backup pools. Most mining firmware lets you configure multiple pool addresses. If the primary pool is unreachable, the miner falls over to the secondary, and then the tertiary. It periodically checks if the primary is back.

Miner attempts to resume:

{
"id": 1,
"method": "mining.subscribe",
"params": ["bmminer/2.0.0", "previous_subscription_id_here"]
}

If the pool still has the session in memory, it may return the same extraNonce1, allowing the miner to resume without losing its place. If the session has expired or the pool does not support resumption, it simply assigns a new session.

On a typical connection, the entire handshake — from TCP connect to receiving the first job — takes well under one second. Here is a realistic timeline:

StepTime from start
TCP handshake (SYN/SYN-ACK/ACK)~20-50 ms
mining.configure (if used)~50-100 ms
mining.subscribe + response~80-150 ms
mining.authorize + response~100-200 ms
mining.set_difficulty received~120-220 ms
mining.notify received~130-230 ms
Miner starts hashing~150-250 ms

These numbers assume a stable internet connection with ~20ms latency to the pool. In practice, most ASICs are hashing within 200-300 milliseconds of connecting. For a machine that runs 24/7, this startup cost is negligible.

To tie this all together, here is what the actual bytes on the wire might look like for a complete connection setup. Each line is one JSON-RPC message (newline-delimited):

Miner —> Pool:

{"id":1,"method":"mining.configure","params":[["version-rolling"],{"version-rolling.mask":"1fffe000","version-rolling.min-bit-count":2}]}

Pool —> Miner:

{"id":1,"result":{"version-rolling":true,"version-rolling.mask":"1fffe000"},"error":null}

Miner —> Pool:

{"id":2,"method":"mining.subscribe","params":["bmminer/2.0.0",null]}

Pool —> Miner:

{"id":2,"result":[[["mining.set_difficulty","1"],["mining.notify","1"]],"abcd1234",4],"error":null}

Miner —> Pool:

{"id":3,"method":"mining.authorize","params":["myaccount.worker1","x"]}

Pool —> Miner:

{"id":3,"result":true,"error":null}

Pool —> Miner:

{"id":null,"method":"mining.set_difficulty","params":[512]}

Pool —> Miner:

{"id":null,"method":"mining.notify","params":["job1","prevhash...","cb1...","cb2...","merkle...","20000000","1a0ffff0","64b1c3a5",true]}

At this point, the miner has everything it needs and begins hashing. The first share submission might come seconds or minutes later, depending on the miner’s hashrate and the share difficulty.

An interesting feature of Stratum is that a single TCP connection can handle multiple workers. This is commonly used by mining proxies — software that sits between a farm of ASICs and the pool. The proxy maintains one connection to the pool but manages dozens or hundreds of ASICs locally.

The flow looks like this:

{"id":3,"method":"mining.authorize","params":["account.worker1","x"]}
{"id":4,"method":"mining.authorize","params":["account.worker2","x"]}
{"id":5,"method":"mining.authorize","params":["account.worker3","x"]}

Each worker is authorized independently, and shares submitted with mining.submit specify which worker found them. The pool tracks hashrate and earnings per worker.

Here are issues you might encounter and what they mean:

“Connection refused” — The pool server is not listening on that port. Check the hostname, port, and that the pool is actually online.

“Connection timeout” — Your network cannot reach the pool. Could be a firewall, ISP issue, or the pool is overloaded. Try a different pool address or port.

Authorization failure — You are connecting fine but the pool rejects your worker name. Double-check your account name, wallet address, or worker format. Some pools require you to create workers in a web dashboard first.

Immediate disconnection after subscribe — This sometimes happens if you are connecting to the wrong port (for example, an SSL port without TLS, or vice versa).

Stale shares after reconnection — If you reconnect and immediately submit shares from the old session, the pool will reject them. Always wait for fresh mining.notify messages after reconnecting.

The Stratum connection lifecycle is a well-defined sequence:

  1. TCP connect to the pool’s host and port
  2. mining.configure (optional) to negotiate protocol extensions like version rolling
  3. mining.subscribe to register and receive extraNonce1 and extraNonce2 size
  4. mining.authorize to authenticate the worker
  5. mining.set_difficulty and mining.notify from the pool to set up initial work
  6. Steady-state loop: receive jobs, hash, submit shares, receive difficulty adjustments

When the connection drops, the miner reconnects with exponential backoff and goes through the entire handshake again. Some pools support session resumption for faster recovery.

In the next article, we will take a deep dive into mining.subscribe and mining.authorize — examining every field, every edge case, and how extraNonce assignment actually works.