Solana DocumentationDeveloping Programs

Anchor Events

Anchor events are typed, Borsh-serialized records included in a program's IDL. They let clients interpret program activity without parsing human-readable log messages.

Events are useful for indexing and notifications, but they are not program state. Store any value that the program must read again in an account, and treat events as a record derived from a successful transaction.

Define and emit an event

Add #[event] to a struct and call emit! after the state change succeeds.

lib.rs
use anchor_lang::prelude::*;
#[program]
pub mod payments {
use super::*;
pub fn record_payment(ctx: Context<RecordPayment>, amount: u64) -> Result<()> {
emit!(PaymentRecorded {
authority: ctx.accounts.authority.key(),
amount,
});
Ok(())
}
}
#[derive(Accounts)]
pub struct RecordPayment<'info> {
pub authority: Signer<'info>,
}
#[event]
pub struct PaymentRecorded {
pub authority: Pubkey,
pub amount: u64,
}

emit! writes the event bytes with Solana's sol_log_data syscall. Transaction logs show the result as Program data: <base64>. The encoded value starts with the event discriminator followed by its Borsh-serialized fields.

Keep events focused. Large strings and vectors consume compute, increase transaction metadata, and are more likely to be affected by log truncation.

Choose between log and CPI events

Anchor supports two emission paths:

PathStorage locationTradeoff
emit!Program data logsSimpler and cheaper, but logs may be truncated
emit_cpi!Inner instruction data from a CPIMore durable metadata, with extra accounts and compute

To use CPI events, enable Anchor's event-cpi feature:

Cargo.toml
[dependencies]
anchor-lang = { version = "1.1.2", features = ["event-cpi"] }

Then annotate the instruction's accounts struct with #[event_cpi] and emit from a handler whose context variable is named ctx:

lib.rs
pub fn record_payment(ctx: Context<RecordPayment>, amount: u64) -> Result<()> {
emit_cpi!(PaymentRecorded {
authority: ctx.accounts.authority.key(),
amount,
});
Ok(())
}
#[event_cpi]
#[derive(Accounts)]
pub struct RecordPayment<'info> {
pub authority: Signer<'info>,
}

#[event_cpi] adds the event authority and current program accounts required by the signed self-CPI. CPI events cannot be consumed through Anchor's log event subscription; fetch the transaction and decode the matching inner instruction instead.

Use the ledger as the source

RPC providers may truncate program logs. If missing an event would affect balances, permissions, or another correctness-critical workflow, derive the result from program accounts or transaction instruction data. Use events to make indexing efficient, not as the only record of state.

Subscribe from TypeScript

The current Anchor TypeScript client is @anchor-lang/core. A Program instance created from the program IDL can decode matching log events and deliver them over the RPC log subscription:

const listenerId = program.addEventListener(
"PaymentRecorded",
(event, slot, signature) => {
console.log({ event, slot, signature });
},
"confirmed"
);
// Remove the websocket subscription when it is no longer needed.
await program.removeEventListener(listenerId);

The callback receives decoded event data, the slot, and the transaction signature. Subscriptions only cover logs delivered while the connection is active. Persist processed signatures and backfill missed transactions after a disconnect if the consumer needs complete history.

Decode fetched transaction logs

For backfills and one-off inspection, fetch a transaction and pass each Program data: payload to the event coder:

const transaction = await connection.getTransaction(signature, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0
});
if (!transaction?.meta || transaction.meta.err) {
throw new Error("Transaction was not found or did not succeed");
}
for (const log of transaction.meta.logMessages ?? []) {
const prefix = "Program data: ";
if (!log.startsWith(prefix)) continue;
const decoded = program.coder.events.decode(log.slice(prefix.length));
if (decoded) console.log(decoded.name, decoded.data);
}

A transaction may invoke several programs. For a production parser, track the Program <address> invoke and success log boundaries or use Anchor's event parser so a different program's Program data: entry is not decoded as yours. Failed transactions can retain logs and inner instructions from work completed before the error, so always reject transaction metadata with a non-null err before indexing either event form. Anchor's log subscription does this check before delivering an event callback.

For an emit_cpi! event, locate the self-CPI in meta.innerInstructions, confirm that its program ID is your program, and base58-decode its instruction data. Before stripping any bytes, verify Anchor's EVENT_IX_TAG_LE:

import bs58 from "bs58";
const EVENT_IX_TAG_LE = Buffer.from("e445a52e51cb9a1d", "hex");
const instructionData = Buffer.from(bs58.decode(innerInstruction.data));
if (!instructionData.subarray(0, 8).equals(EVENT_IX_TAG_LE)) {
// This is an ordinary self-CPI, not an emit_cpi! event.
continue;
}
const eventData = instructionData.subarray(8).toString("base64");
const decoded = program.coder.events.decode(eventData);

Checking both the program ID and tag prevents an ordinary self-CPI from being mistaken for an event.

Version event schemas

By default, Anchor derives an 8-byte event discriminator from the event name. Anchor also supports an explicit discriminator through #[event(discriminator = ...)]. Discriminators must remain unique within the program and stable for every client that decodes the event.

Changing an emitted struct's fields changes its Borsh layout. For a breaking schema change, define a new event such as PaymentRecordedV2 rather than silently changing PaymentRecorded. Keep emitting the old event during a migration window when existing indexers still depend on it.

For evolvable data, prefer fixed-width fields and identifiers that let an indexer fetch richer account data separately. Publish the updated IDL before or with the program upgrade so consumers can decode the new event immediately.

Test events

Test the decoded contract, not the base64 string. Register the listener before sending the instruction, assert the decoded fields and transaction signature, and always remove the listener:

let listenerId = 0;
const received = new Promise<{
event: { authority: PublicKey; amount: BN };
signature: string;
}>((resolve) => {
listenerId = program.addEventListener(
"PaymentRecorded",
(event, _slot, signature) => resolve({ event, signature }),
"confirmed"
);
});
try {
const signature = await program.methods.recordPayment(new BN(500)).rpc();
const result = await received;
assert.equal(result.signature, signature);
assert.equal(result.event.amount.toString(), "500");
} finally {
await program.removeEventListener(listenerId);
}

Also test that the indexer ignores event bytes retained in failed transaction metadata, and add a fixture for every supported event version. If you use emit_cpi!, test decoding from innerInstructions because log listeners do not receive those events directly.

Further reading

Is this page helpful?

Table of Contents

Edit Page
© 2026 Solana Foundation. All rights reserved.